尚学堂视频笔记六:多线程

多线程学习笔记

    • 1. 开篇
      • 1.1 目录
    • 2. 线程介绍
      • 2.1 那么多线程和方法的调用有哪些区别呢?
      • 2.2 程序、进程与线程
      • 2.3 进程和线程
      • 2.4 核心概念:
    • 3. 线程的创建
      • 3.1 Thread类
      • 3.2 Runner
      • 3.3 常用的两种方式:
        • 3.3.1 继承Thread方法创建线程
          • 运行代码示例
            • 示例一
            • 示例二
        • 3.3.2 重写Runnable方法来创建线程
          • 示例一:
          • 示例二:
          • 实例三:
      • 3.4 了解Callable
        • 示例一:
        • 示例二:
      • 3.5 静态代理设计模式
      • 3.6 推导lambda-简化线程
    • 4.线程状态
      • 4.1 线程的运行的特点
      • 4.2 线程方法
      • 4.3 线程创建
      • 4.4 就绪状态
        • 第一种方式:
        • 第二种方式:
      • 4.5 线程运行
      • 4.6 线程停止
      • 4.7 阻塞状态
        • 4.7.1 sleep方法
          • 具体的使用方式
          • 使用的场景:
            • 用来模拟网络延时--放大问题:
            • 模拟休息:
            • 模拟倒计时:
        • 4.7.2 Join方法
        • 4.7.3 深度观察线程的状态
      • 4.8 线程优先级
      • 4.9 线程的分类
        • 4.9.1 守护进程(daemon)
      • 4.10 常用方法
    • 5. 线程的同步-并发控制
      • 5.1 引入
      • 5.2 概念
      • 5.3 非同步的三大经典案例
      • 5.5 多线程并发与同步 队列与锁
        • 5.5.1 线程同步
          • 接下来我们来看代码:
        • 5.5.2 同步块
        • 5.5.3 并发的几个经典例子
      • 5.6 死锁-产生和解决
        • 5.6.1 概念
        • 5.6.2 例子和讲解
      • 5.7线程协作(cooperation)-生产者消费者模式
        • 5.7.1 线程通信
          • 生命周期
          • 例子
            • 管程法例子:
            • 信号灯例子
    • 6. 高级主题-更上一层楼!
      • 6.1 任务定时调度
        • 6.1.1 Timer 和 TimerTask
        • 6.1.2 QUARTZ
      • 6.2 HappenBefore
        • 6.2.1 引入
        • 6.2.2 指令重排:
        • 6.2.3 数据依赖
          • 6.3.1 概念
      • 6.3 Volatile
      • 6.4 dcl单例模式-设计模式
      • 6.5 ThreadLocal-每个线程本地存储区域
      • 6.6可重入锁原理的实现:
        • 6.6.1 概念
        • 6.6.2 可重入锁的原理的实现:
      • 6.7 CAS-原子操作
        • 6.7.1 锁的分类:
        • 6.7.2 Compare and Swap 比较并交换

1. 开篇

​ 多线程是任何一门编程语言的重要特性。在Java中大量应用于网络开发和桌面应用的开发,可以这么说多线程在开发中无处不在。小到自己编写的软件,大到系统的底层源码,都用到了多线程。

​ 大家都知道,电商抢购,春节期间购票等,都必须精准且不能出错。这些就是我们常说的"三高"程序。"三高"程序指这个网站或者程序满足以下三个特点:

  1. 高可用:不能出错。
  2. 高性能:用户体验要好,同时处理的速度要快。
  3. 高并发:同时支持大量的用户进行操作。

​ “三高” 程序是我们进入中高级架构师的必经之路,也是比较难以把握的技术。既要保证多人操作不能出错,又要保证效率。

1.1 目录

  • 线程介绍
  • 线程实现
  • 线程状态
  • 线程同步
  • 生产消费者
  • 高级主题

2. 线程介绍

线程指的是多条路径同时进行,有多条路径,那么就是说有多个任务。也就是说,必须有多个任务,我们才有必要引入多线程。可以说,处理多任务,正是我们引入多线程的出发点。

​ 现实中有太多多线程的例子:比如,我们可以一边听着歌一边敲代码;一边打电话一边打点滴;一边听歌一边睡觉等。

​ 然而,看似是多任务同时进行,但是实际上在同一个时间点我们的大脑还是只是在做了同一件事情。只不过由于我们在很短的时间内干了多件事情,给自己的感觉是在同时进行多个任务

​ 所以,必须要两个人或者两台电脑,或者这台电脑里面的CPU中有多个核心,才能同时进行多个任务,要不然,我们还是在同一个时间点,做了一件事情

​ 也可以将多线程看成一条马路。原来是一条路,因为车多了,为了提高使用效率,充分使用这条道路,中间加了栅栏,变成了多条道路,所有的车共享这一条路。

​ 多线程在游戏领用领域应用非常广泛,比如在同一个界面中操作多个角色和不同动作。

​ 那么我们怎么理解多线程呢?

​ 比如我们这里有三个电器,接在同一个电线上,打开任何电源,都能同时亮起,同时工作;而年终发奖品,所有的同事都去拿奖品等。这些就是多线程。

​ 同理,当我们在上网的时候。如果没有多线程,必须等待A用户使用完毕后,结束对网络的使用,B用户才能进入;同样的B用户结束对网络的使用,C用户才能进入。这样,ABC之间就是一条路了,这就是单线程。真正的多线程就是开辟了多条路,大家个上个的网,互相不影响。

2.1 那么多线程和方法的调用有哪些区别呢?

​ 它们之间的区别是很大的。

​ 方法间的调用:普通方法调用,从哪里来多哪里去,闭合的一条路径

​ 多线程使用:开辟了多条路径。

尚学堂视频笔记六:多线程_第1张图片

​ 方法间的调用:比如在main() 方法中,调用邮件编写的方法,而邮件编写方法又调用上传附件的方法,这时main() 方法必须等待邮件编写方法完成,而邮件编写方法必须等待附件上传方法执行完成。执行完成之后再依次的返回,最终才能继续执行main() 方法。

​ 而在并行的两条路径中,main() 方法调用了邮件编写方法后,就不用等待邮件编写和附件上传方法完成之后再继续执行。而是直接开启另一条线程,去执行邮件编写和附件上传方法,main() 方法继续向下执行。

2.2 程序、进程与线程

​ 那么多任务也就是多线程是在哪里开辟的呢?

​ 是指在我们的进程中,也只在我们的程序中。

​ 程序也就是我们写的代码了,比如我们上优酷,播放视频,那么这个编写的播放视频的代码就是程序。

​ 进程是指什么?进程指我们的操作系统开始运行这个程序就把他成为进程。也就是说,一个进程就是一个程序。一个进程可以有多个线程,如视频中中同时听到声音、开图像、显示字幕。

​ 程序是一个静态的概念,就是指我们的代码。进程指动态的概念,我们的cpu调度到了。一个进程匹配一个程序。线程指的是一个进程开辟多条路径,充分利用我们的CPU。

2.3 进程和线程

区别 进程 线程
根本区别 作为资源分配的单位。 调度和执行的单位。
开销 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。 线程可以看成轻量的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。
所处环境 在操作系统中能同时运行多个任务(程序)。 在同一程序中有多个顺序流同时执行。
分配内存 系统在运行的时候会为每个进程分配不同的内存区域。 除了CPU之外,不会为线程分配内存(线程所使用的资源是他所属的进程的资源),线程组只能共享资源。
包含关系 没有线程的进程是可以被看做单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而是多条(线程)共同完成。 线程是进程的一部分,所以线程有的时候被称为轻权进程或者轻量级进程。

注意:很多多线程是模拟出来的,真正的多线程是指由多个cpu,即多核,如服务器。如果是一个模拟出来的多线程,即一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。

2.4 核心概念:

  • 线程就是独立的执行路径;
  • 在程序运行时,即是没有自己创建线程,后台也会存在多个线程,如gc线程、主线程;
  • main() 称之为主线程,为系统的入口点,用于执行整个程序;
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是于操作系统紧密相关的,先后顺序是不能认为的干预的;
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销;
  • 每个线程在自己工作内存交互,加载和存储主内存控制不当会造成数据不一致。

3. 线程的创建

​ 在我们之前写的代码,都是单线程,由主方法调用,调用完成后,沿一条路原路返回,而创建线程,在Java中有对应的API。

​ 在Java中,创建多线程有三种方式:

  1. 继承Thread类,重写Run() 方法;
  2. 实现Runnable接口,重写Run() 方法;
  3. 实现Callable接口,重写Call() 方法;

​ 第三种是juc并发包下的,我们使用的并不多,了解即可。重点掌握前面的两种方法。由于在Java中不存在多继承,所以我们要多实现接口,而不是继承类。所以我们多用Runnable接口。

3.1 Thread类

以下为jdk1.8中的说明:

​ 线程是程序中执行的线程。Java虚拟机允许应用同时执行多个执行线程。

​ 每个线程都有优先权。具有较高优先级的线程优先于优先级较低的线程执行。每个线程可能也可能不被标记为守护程序。当在某个线程中运行的代码创建一个新的Thread对象时,新线程的优先级最初设置为创建线程的优先级,并且当且仅当是守护进程是才是守护进程。


线程可以被分为两类:用户线程 ( User Thread )和守护线程 ( Daemon Thread )。

​ Java平台把操作系统的底层进行了屏蔽,在JVM虚拟平台里面构造出对自己有利的机制,这就是守护进程的由来。Daemon的作用是为其他线程的运行提供服务,比如GC线程(垃圾回收进程)。

​ User Thread线程和Daemon Thread唯一的区别之处就在虚拟机的离开,如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。

​ 守护线程用户可以自行设定,方法 public final void setDaemon(boolean flag)。

​ 注意点:

  • 正在运行的常规线程不能设置为守护线程。

  • thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。

  • 在Daemon线程中产生的新线程也是Daemon的 ( 这里要和linux的区分,linux中守护进程fork()出来的子进程不再是守护进程 ) 。

  • 根据自己的场景使用 ( 在应用中,有可能你的Daemon Thread还没有来得及进行操作,虚拟机可能已经退出了)

    摘自 不陌博客,用户线程和守护线程:

https://www.cnblogs.com/weishao-lsv/p/8143976.html


​ 当Java虚拟机启动时,通常有一个非守护进程线程(通常调用某些指定类的名为main的方法)。Java虚拟机将继续执行线程,知道发生以下任一情况:

  • 已经调用了Runnable类的exit方法,并且安全管理器已经允许进行退出操作。
  • 所有不是守护进程线程的线程都已经死亡,无论是从调用返回到run方法还是跑出run方法的run。

​ 创建一个新的执行线程有两种方法。一个是将一个类声明为Thread的子类。这个子类应该重写Thread类的run方法。然后可以分配并启动类的实例。例如,计算大于规定值的素数的线程可以写成如下:

class PrimeThread extends Thread {
    long minPrime;
    PrimeThread(long minPrim) {
        this.minPrime = minPrime;
    }
    
    public void run() {
        // compute primes larger than minPrime
    }
}

​ 然后,以下代码将创建一个线程并启动它运行:

PrimeThread p = new PrimeThread(143);
p.start();

​ 另一种方法来创建一个线程是声明实现类Runnable接口。那个类然后实现了run方法。然后可以分配类的实例,在创建Thread时作为参数传递,并启动。这种其他风格的同一个例子如下 :

class PrimeRun implements Runnable {
    long minPrime;
    PrimeRun(long minPrime) {
        this.minPrime = minPrime;
    }
    
    public void run() {
    	// compute prime large than minPrime
    }
}

​ 然后,以下代码将创建一个线程并启动它:

PrimeRun p = new PrimeRun(143);
new Thread(p).start();

​ 每个线程都有一个用于识别目的的名称。多个线程可能具有相同的名称。如果在创建线程时为指定名称,则会成为其是一个新名称。

​ 除非另有声明,否则将null参数传递给null中的构造函数或方法将导致抛出NullPointerException。

嵌套类摘要

修饰语和类型 类和描述
static class Thread.State线程状态
static interface Thread.UncaughtExceptionHandler
当Thread由于未捕获的异常而突然中止时,处理程序的接口被调用。

Field Summary

修饰语和类型 域和修饰符
static int MAX_PRIORITY
线程可以拥有的最大优先级
static int MIN_PRIORITY
线程可以拥有的最小优先级
static int NORM_PRIORITY
分配给线程的默认优先级

构造方法摘要

构造方法 描述
Thread() 分配一个新的Thread对象
Thread(Runnable target) 分配一个新的Thread对象
Thread(Runnable target, String name) 分配一个新的Thread对象
Thread(String name) 分配一个新的Thread对象
Thread(ThreadGroup group, Runnable target) 分配一个新的Thread对象
Thread(ThreadGroup group, Runnable target, String name) 分配一个新的Thread对象,使其具有target作为其运行对象,具有指定的name作为其名称,属于group引用的线程组。
Thread(TreadGroup group, Runnable target, String name, long stackSize) 分配一个新的Thread对象,一边它具有target作为其运行对象,将指定的name正如其名,一边属于该线程组又称作 group,并具有指定的堆栈大小。
Thread(ThreadGroup group, String name) 分配一个新的Thread对象

方法摘要(只摘要部分常用的)

修饰词和类型 方法名 描述
String getName() 返回此线程的名称
long getId() 返回此线程的标志符
int getPriority() 返回此线程的优先级
static void sleep(long millis) 使当前正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精确度和准确性。
void start() 导致此线程开始执行;Java虚拟机调用此线程的run方法。
static void yield() 对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。
void run() 如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run方法;否则,此方法不执行任何操作并返回。

3.2 Runner

​ Runnable 接口应由任何实现类,其实例将由线程执行。该类必须定义一个无参数的方法,称为run。

​ 该接口旨在为希望在活动时执行代码的对象提供一个通用协议。例如,Runnable有Thread类Thread。活跃的只是意味着一个线程已经启动,还没有被停止。

​ 另外,Runnable提供了一个类被激活而不是Thread Thread类化的Thread。一个实现类Runnable可以在不继承运行Thread实例化一个Thread实例,并在传递本身作为目标。在大多数情况下,Runnable接口应使用,如果你只打算重写run() 方法并没有其他Thread方法。这时重要的,因为类不应该被子类化,除非程序员打算修改或增强类的基本行为。

方法摘要

修饰词和类型 方法和描述
void run()
当实现接口的对象Runnable被用来创建一个线程,启动线程时对象的run在独立执行的线程中调用方法。

方法详情

方法 描述
run void run()
当实现接口的对象Runnable被用来创建一个线程,
启动线程对象的run在独立执行的线程中调用的方法。
方法run的一般合同是它可以采取人和行动。

3.3 常用的两种方式:

  1. 继承Thread方法,重写Runnable接口中的run方法,创建实例后,调用start方法来启动线程
  2. 实现Runnable接口,重写run方法,通过Thread对象来调用start方法。必须调用Thread对象,这里的Thread对象我们成为代理对象。

3.3.1 继承Thread方法创建线程

运行代码示例
示例一
/**
 * 创建线程方式一:
 * 1. 继承Thread重写run
 * 2. 启动:创建子类对象 + start
 */

public class StartThread extends Thread {
    /**
     * 线程入口点
     */
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("一边听歌");
        }
    }

    public static void main(String[] args) {
        // 创建子类对象
        StartThread st = new StartThread();
        // 启动
        st.start();// 不保证不立即运行 cpu调用
        for (int i = 0; i < 20; i++) {
            System.out.println("一边coding");
        }
    }
}

对该方法的一些简单的分析:

​ main方法进入方法区,然后在内存中创建对象,然后调用实例化对象中的start方法,开启一个新的线程,交给CPU去调用。然后继续运行后面的for循环。此时结果并不固定。

​ 如果将start方法更改为run方法(变为普通的方法调用),不是开启多线程,而是变为普通方法的调用。

​ start方法会自己调用run方法。

示例二

下载图片

package com.thread01;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * 下载图片
 */
public class WebDownloader {
    /**
     * 下载
     * @param url
     * @param name
     */
    public void download(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (MalformedURLException e) {
            e.printStackTrace();
            System.out.println("不合法的URL");
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("图片下载失败");
        }
    }

}

package com.thread01;

public class TDownloader extends Thread {
    private String URL; // 远程路径
    private String name; // 存储名字

    public TDownloader(String URL, String name) {
        this.URL = URL;
        this.name = name;
    }

    @Override
    public void run() {
        WebDownloader wd = new WebDownloader();
        wd.download(URL, name);
        System.out.println(name);
    }

    public static void main(String[] args) {
        TDownloader td1 = new TDownloader("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564048573422&di=dbf59ed10d887489b3c3a48e2d76c174&imgtype=0&src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F0b91b92f8fb1d93fdfcfcbafa4eab115365118236b929-jj5AiK_fw658","pic/1.gif");
        TDownloader td2 = new TDownloader("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564643346&di=1ccbf37061991dfac345e8de8abaa153&imgtype=jpg&er=1&src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F2c0f0b3816463f804a08d11e80873098c45bc8f4f6c9d-S3wo4O_fw658","pic/2.gif");
        TDownloader td3 = new TDownloader("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564048664710&di=66efe336b9911be0f79ea4dcb5b7aa3c&imgtype=0&src=http%3A%2F%2Fgss0.baidu.com%2F94o3dSag_xI4khGko9WTAnF6hhy%2Fzhidao%2Fwh%253D450%252C600%2Fsign%3Da386a06a7ef0f736d8ab44053f659f2f%2Fb03533fa828ba61ea0315e3a4734970a314e59c3.jpg","pic/3.gif");

        // 启动三个线程
        td1.start();
        td2.start();
        td3.start();
    }
}

3.3.2 重写Runnable方法来创建线程

​ 这种方式是我们推荐的方式,避免了继承的局限性。

​ 这个实现还是很简单的,只需要将继承Thread类改为实现Runnable接口,在启动线程时,实例化一个Thread代理对象进行调用即可。

public class TDownloader implements Runnable {
    private String URL; // 远程路径
    private String name; // 存储名字

    public TDownloader(String URL, String name) {
        this.URL = URL;
        this.name = name;
    }

    @Override
    public void run() {
        WebDownloader wd = new WebDownloader();
        wd.download(URL, name);
        System.out.println(name);
    }
}
示例一:
/**
 * 创建线程方式二:
 * 1. 创建:实现Runnable + 重写run
 * 2. 启动:创建实现类对象 + Thread对象 + start
 */
public class StartRun implements Runnable{

    /**
     *
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("一边听歌");
        }
    }

    public static void main(String[] args) {
        // 创建实现类对象
        StartRun sr = new StartRun();
        // 创建代理类对象
        Thread thread = new Thread(sr);
        thread.start();
        // 也可以借助匿名类对象
        // new Thread(new StartRun()).start();
        for (int i = 0; i < 10; i++) {
            System.out.println("一边敲代码");
        }
    }
}

使用重写Runnable方法来创建线程的过程如下:

  1. 创建:实现Runnable接口并且重写run()方法。
  2. 启动:创建实现类对象,创建Thread代理类对象,将实现类对象丢入Thread代理类的构造方法中。
  3. 最后调用Thread类的start方法。

我们在实现多线程时

优点:

  1. 为了避免单继承的局限性,推荐使用接口

  2. 方便共享资源

示例二:
/**
* 这里通过使用Runnable,我们就可以共享资源
*/

/**
 *  共享资源 (并发 要保证线程安全)
 * @author QianQian
 *
 */

public class Web12306 implements Runnable{
	// 票数
	private int ticketNums = 99;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			if (ticketNums < 0) {
				break;
			}
			try {
			Thread.sleep(200);
			}catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread() .getName() + "--->" + ticketNums --);
		}
	}
	public static void main(String[] args) {
		// 一份资源
		Web12306 web = new Web12306();
		// 多个代理
		new Thread(web, "黄牛1").start();
		new Thread(web, "黄牛2").start();
		new Thread(web, "黄牛3").start();
	}
}
实例三:
/**
* 模拟龟兔赛跑
*/
package com.qianqian.thread;

/**
 * 模拟龟兔赛跑
 * @author QianQian
 *
 */
public class Racer implements Runnable {
	// 胜利者
	private static String winner;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int steps = 1; steps <=100; steps ++) {
			// 模拟休息
			if (Thread.currentThread().getName().equals("rabbit") && (steps % 10 == 0)) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + "--->" + steps);
			// 比赛是否结束
			boolean flag = gameOver(steps);
			if (flag) {
				break;
			}
		}
	}
	
	private boolean gameOver(int steps) {
		if (winner != null) {
			return true; // 存在胜利者
		}else {
			if (steps == 100) {
				winner = Thread.currentThread().getName();
				System.out.println("winner ==>" + winner);
				return true;
			}
		}
		return false;
	}

	public static void main(String[] args) {
		Racer racer = new Racer();
		new Thread(racer, "tortoise").start();
		new Thread(racer, "rabbit").start();
	}
	
}
/** 结果:

3.4 了解Callable

​ 这是实现多线程编程的第三种方式,实现Callable接口,重写call方法。由于这个属于高级的并发性编程——juc编发编程的内容,不属于初级阶段的行列,所以稍作提及。这是我们工作了3-5年之后才会设计的并发编程的领域。现在讲解,是为了面试用的。同时,我们要了解取舍,什么阶段完成什么样的任务,不要给自己太大的压力。

​ 那么Callbale的call方法比我们的Runnable的run方法强大在哪里呢?

实现Callable

class CDownloader implements Callable {
    private String url;
    private String fname;
    public CDownloader (String url, String fname) {
        this.url = url;
        this.fname = fname;
    }
    public Object call() throws Exception {
        WebDownloader downloader = new WebDownloader();
        downloader.download(url, fname);
        return true;
    }
}

使用

1. 创建目标对象: CDownloader = new CDownloader("图片地址", "baidu.png");
2. 创建执行服务: ExecutorService ser = Executors.newFixedThreadPool(1);
3. 提交执行: Future<Boolean> result = ser.submit(cd1);
4. 获取结果: boolean r1 = result.get();
5. 关闭服务: ser.shutdownNow();

它强在可以抛出异常,而Runnable的run方法是不可以的。同时呢,它是可以拥有返回值的,而我们的Runnbale方法,返回值是void。相比起Runnable,Callable想要创建线程更加麻烦,需要借助执行服务( ExecutorService ser)线程池( Executors.newFixedThreadPool() )。使用起来比起Runnable更加强大,但是稍微麻烦一点。同时,需要提交到我们的服务里面,我们所创建的未来的池,

Future<Boolean>

然后我们通过get方法来返回,最后需要关闭服务。

示例一:

/**
* 修改之前下载图片的例子
*/

import java.util.concurrent.*;

public class CDownloader implements Callable<Boolean> {
    private String URL; // 远程路径
    private String name; // 存储名字

    public CDownloader(String URL, String name) {
        this.URL = URL;
        this.name = name;
    }

    @Override
    public Boolean call() throws Exception {
        WebDownloader wd = new WebDownloader();
        wd.download(URL, name);
        System.out.println(name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CDownloader td1 = new CDownloader("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564048573422&di=dbf59ed10d887489b3c3a48e2d76c174&imgtype=0&src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F0b91b92f8fb1d93fdfcfcbafa4eab115365118236b929-jj5AiK_fw658","pic/1.gif");
        CDownloader td2 = new CDownloader("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564643346&di=1ccbf37061991dfac345e8de8abaa153&imgtype=jpg&er=1&src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F2c0f0b3816463f804a08d11e80873098c45bc8f4f6c9d-S3wo4O_fw658","pic/2.gif");
        CDownloader td3 = new CDownloader("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564048664710&di=66efe336b9911be0f79ea4dcb5b7aa3c&imgtype=0&src=http%3A%2F%2Fgss0.baidu.com%2F94o3dSag_xI4khGko9WTAnF6hhy%2Fzhidao%2Fwh%253D450%252C600%2Fsign%3Da386a06a7ef0f736d8ab44053f659f2f%2Fb03533fa828ba61ea0315e3a4734970a314e59c3.jpg","pic/3.gif");

        // 创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(3);
        // 提交执行:
        Future<Boolean> result1 = ser.submit(td1);
        Future<Boolean> result2 = ser.submit(td2);
        Future<Boolean> result3 = ser.submit(td3);
        // 获取结果
        boolean r1 = result1.get();
        boolean r2 = result1.get();
        boolean r3 = result1.get();
        System.out.println(r3);
        // 关闭服务
        ser.shutdownNow();
    }
}

示例二:

/*
* 修改之前龟兔赛跑的例子
*/
import java.util.concurrent.*;

/**
 * 模拟龟兔赛跑
 * @author QianQian
 *
 */
public class CRacer implements Callable<Integer> {
	// 胜利者
	private static String winner;
	@Override
	public Integer call() throws Exception{
		// TODO Auto-generated method stub
		for(int steps = 1; steps <=100; steps ++) {
			// 休息
			if (Thread.currentThread().getName().equals("pool-1-thread-1") && (steps % 10 == 0)) {
				Thread.sleep(1000);
			}
			System.out.println(Thread.currentThread().getName() + "--->" + steps);
			// 是否结束
			boolean flag = gameOver(steps);
			if (flag) {
				return steps;
			}
		}
		return null;
	}
	
	private boolean gameOver(int steps) {
		if (winner != null) {
			return true; // 存在胜利者
		}else {
			if (steps == 100) {
				winner = Thread.currentThread().getName();
				System.out.println("winner ==>" + winner);
				return true;
			}
		}
		return false;
	}

	public static void main(String[] args) throws ExecutionException, InterruptedException {
		CRacer racer = new CRacer();
		// 创建执行服务
		ExecutorService ser = Executors.newFixedThreadPool(2);
		// 提交执行
		Future<Integer> result1 = ser.submit(racer);
		Future<Integer> result2 = ser.submit(racer);
		// 获取结果
		Integer r1 = result1.get();
		Integer r2 = result2.get();
		System.out.println(r1 + "--->" + r2);
		// 关闭服务
		ser.shutdownNow();
	}

}

3.5 静态代理设计模式

​ 我们在使用Runnable接口实现run接口时,必须使用Thread类对象,我们将这个Thread类对象称为代理对象。这个代理是静态代理。这个静态代理也是一种设计模式,也比较常见。

​ 代理分为静态代理和动态代理,现在了解静态代理。

​ 代理在现实中也很常见,比如婚礼:

​ 婚庆公司在婚礼前和婚礼后忙前忙后,最后结婚还是你。

  • 你:真实角色
  • 婚庆公司:代理角色,帮你搞婚庆
  • 结婚礼仪:实现相同的接口

​ 比如去外面上java培训班,培训班帮你准备资料,最后学习的还是你。

​ 代理在我们开发一般用于记录日志,增强服务,在后期中用的还是很多的。

​ 那么静态代理和动态代理区别

​ 静态代理是我们之前预先写好的,而动态代理是在运行中动态构建出来的。

​ 今天研究静态代理。

​ 比如,写一个婚庆公司的简单的Java程序:

/**
 * 静态代理
 * 必须实现公共的接口:
 * 1. 真是角色
 * 2. 代理角色
 */
public class StaticProxy {
    public static void main(String[] args) {
        new WeddingCompany(new You()).happyMarry();
    }
}

interface Marry {
    void happyMarry();
}

class You implements Marry {

    @Override
    public void happyMarry() {
        System.out.println("终于结婚了");
    }
}

class WeddingCompany implements Marry {
    // 真实角色
    private Marry target;
    public WeddingCompany(Marry target) {
        this.target = target;
    }
    @Override
    public void happyMarry() {
        ready();
        this.target.happyMarry();
        after();
    }

    private void ready() {
        System.out.println("布置会场");
    }

    private void after() {
        System.out.println("收拾会场");
    }
}

// 这个就很像装饰器设计模式了。
// 装饰器设计模式:
/*
* 1. 抽象组件
* 2. 具体组件
* 3. 抽象装饰类
* 4. 具体装饰类
* 装饰器设计模式用来增强具体组件的功能。
* 又分为透明和半透明模式
* 透明: 装饰类只实现了抽象组件中的方法
* 半透明: 装饰类除了实现抽象组件中的方法,可能还额外增加了新的方法

​ 这个代理模式在后面我们一定会用来记录谁登录了,谁退出了。

​ 或者你用了多少内存,使用代理模式来监视内存使用状况。

这个设计模式请大家好好的琢磨一下。

3.6 推导lambda-简化线程

​ 这个JDK8中的新特性。

  • λ希腊字母表中排序第十一位,应为字母为lambda
  • 避免匿名内部类定义过多
  • 其实质属于函数式编程

​ 接下来我们推导一下为什么我们需要lambda:

/**
 * Lambda表达式 简化线程(用一次)的使用
 */

public class LambdaThread{

    // 1.静态内部类, 只有当LambdaThread编译时才会进行编译。要不然放着不动。
    static class Test implements Runnable {
        @Override
        public void run() {
            for(int i = 0; i < 20; i++) {
                System.out.println("一边唱歌");
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new Test()).start();

        // 2.局部内部类,方法内部的。
        class Test implements Runnable {
            @Override
            public void run() {
                for(int i = 0; i < 20; i++) {
                    System.out.println("一边唱歌");
                }
            }
        }

        // 3.匿名内部类  必须借助接口或者父类
        new Thread (new Runnable(){
            @Override
            public void run() {
                for(int i = 0; i < 10; i++) {
                    System.out.println("一边唱歌");
                }
            }
        }).start();

        // 4.JDK8进行简化  lambda表达式
        new Thread(()->{
            for(int i = 0; i < 20; i++) {
                System.out.println("一边唱歌");
            }
        }).start();
    }
}
  1. 如果这个线程我们就是用一次,我们就可以使用静态内部类,它的好处就是只有当外部类需要被使用时,才会编译。
  2. 但是,其他类的内部类我们还要创建一个新的对象,不如使用局部内部类,这时方法内部的类。
  3. 仅仅使用一次我们就需要完整地写一个局部内部类,所以我们使用匿名内部类。
  4. 匿名内部类需要使用接口接口或者父类,意味着必须重写父类方法或者实现接口才能使用,我们就必须关注父类或者接口中必须重写或者实现的方法。
  5. 所以在JDK8,推出了lambda表达式,我们只需要写下我们的方法即可。当然,这个表达式适合进行简单的线程简化。

​ lambda表达式有一个特点,就是只能用于实现接口中只有一个方法的情况。当接口中存在2个或者2以上的方法时,JDK8推导不了。

以下是一些使用lambda的例子:

没有返回值的例子

/**
 * Lambda
 */

public class LambdaTest01 {
    public static void main(String[] args) {
        ILike like = ()-> {
            System.out.println("i like lambda5");
        };
        like.lambda();
    }
    // 静态内部类
    static class Like2 implements ILike {
        @Override
        public void lambda() {
            System.out.println("i like lambda2");
        }
    }
}

interface ILike {
    void lambda();
}

class Like implements ILike {

    @Override
    public void lambda() {
        System.out.println("i like lambda");
    }
}

有返回值的例子并且有参数:

/**
 * Lambda
 */

public class LambdaTest02 {

    public static void main(String[] args) {
        ILove love = (int a) -> {
            System.out.println("i like" + a);
        };

        love.lambda(100);

        // 简化
        love = (a) -> {
            System.out.println("i like" + a);
        };
        love.lambda(5);

        // 更加简化
        love = a -> System.out.println("i like " + a);
        love.lambda(50000);

        // 传参
        IInterest interest = (int a, int c) -> {
            System.out.println("23");
            return a + c;
        };

        interest = (a, c) -> {
            System.out.println("var");
            return a + c;
        };
    }


}

interface ILove {
    void lambda(int a);
}

class Love implements ILove {

    @Override
    public void lambda(int a) {
        System.out.println("I love");
    }
}

interface IInterest {
    int lambda(int a, int c);
}

class Interest implements IInterest {

    @Override
    public int lambda(int a, int c) {
        return a + c;
    }
}

4.线程状态

​ 线程一共有5大状态
尚学堂视频笔记六:多线程_第2张图片

​ 可以用踢球做一个简单比喻,新生状态表示球员入选;就绪状态表示球员在球场上时刻装备接球;运行状态表示球员接到球正在运球;等待阻塞表示球员已经摔倒,而后爬起重新进入就绪状态;死亡状态表示球员被下场。

4.1 线程的运行的特点

  1. 线程进入阻塞状态,阻塞状态结束后,并不是马上就切换成运行状态,而是切换成就绪状态。
  2. 线程进入死亡状态后,不能重启开启的。如果你重新开启,那么是一个新的线程,而不是原先的线。

​ 那么有哪些方法会进入那些状态呢?

尚学堂视频笔记六:多线程_第3张图片

  1. 一旦创建了线程对象,那么该线程就进入了新生状态。进入了新生状态,每个线程就用了自己的内存空间。就是线程的工作空间。工作空间与主内存进行数据交互。

  2. 一旦调用了start方法,就进入了就绪状态了。但是能否执行必须看CPU的调度。就绪状态只能表示该线程具备了运行的条件,处于就绪队列中。就绪状态不是运行状态。只有当CPU调度到了,才会进入运行状态。

    一般来说有四种方法,使一个线程编程就绪状态

    • 调用start方法
    • 阻塞状态解除
    • yield方法中断该线程运行,重新进入就绪状态
    • jvm通过自己的算法,将该线程切换到其他线程。那么该线程重新进入就绪状态
  3. 被CPU调度到了,便会进入运行状态(由CPU控制我们不能控制),这时才真正地执行线程体的代码块。当我们打印线程名字时,jvm将就绪状态和运行状态统一称为Runnable。

  4. 阻塞状态也有4原因,导致阻塞状态发生(线程状态内部又将这些阻塞作区分–在内部类Thread.State中给出,给了时间的为 time_waiting):

    • 调用sleep方法:俗话称为:“抱着资源睡觉”,不会让出资源。(–>waiting, 给了时间 time_waiting)
    • 调用wait方法:不占用资源。(–> blocked)
    • 调用join方法:等待其他线程运行完成在运行该线程。 (–> waiting, 给了时间time_waiting)
    • 部分操作也会进入阻塞状态,比如IO里面的read和write。
  5. 死亡状态是生命周期的最后状态,死亡的原因一般有两个:

    • 一个是线程执行完成。
    • 第二个是强制中断该线程的运行,强制其进入死亡状态。比如stop和destroy方法。这两个方法一般不推荐使用,因为起过时了,不安全。

4.2 线程方法

​ 一般来说,我们操作线程,还是通过线程方法:

  • sleep方法
    • 使线程停止运行一段时间,将处于阻塞状态。
    • 如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行。
  • join方法
    • 阻塞指定线程等待另一个线程完成后再继续执行。
  • yield方法
    • 让当前正在执行的线程暂停,不是阻塞线程,而是将线程转入就绪状态;
    • 调用了yield方法之后,如果没有其他等待执行的线程,此时当前线程就会马上恢复执行。
  • setDaemon方法
    • 可以将指定的线程设置成后台线程,守护线程;
    • 创建用户线程的线程结束时,后台线程也随之消亡;
    • 只能在线程启动之前把它设置成为后台线程。
  • setPriority(int newPriority) getPriority方法
    • 线程的优先级代表的是概率
    • 范围从1到10,默认为5。
  • stop方法
    • 停止线程
    • 不建议使用
    • 一般我们想停止一个线程,有两种方法,让这个线程执行和想方设法将这个线程执行完

4.3 线程创建

​ 线程的状态之一:新生状态。

​ 通过继承Thread类或者重写Runnable接口,并示实例化该对象(也就是new一个对象),来创建一个新的线程,该线程会处于新生状态。

4.4 就绪状态

第一种方式:

​ 实例化继承Thread或者重写Runnable接口的类,并调用start方法,来使线程处于就绪状态。

第二种方式:

yield(避免一个线程暂用cpu时间太久)

  • 礼让线程,让当前正在执行线程暂停;
  • 不是阻塞线程,而是将线程从运行状态转入就绪状态;
  • 让cpu重新调度。
修饰词和类型 方法名 描述
static void yield() 对调度程序的一个暗示,即当前线程愿意让出当前使用的处理器。

使用方式如下:

public class YieldDemo01 extends Thread {
    public static void main(String[] args) {
        YieldDemo01 demo = new YieldDemo01();
        Thread t = new Thread(demo);
        t.start();
        for(int i = 0; i < 1000; i++) {
            if (i % 20 == 0) {
                // 暂停该线程 main
                Thread.yield();
            }
            System.out.println("main...." + i);
        }
    }
    @Override
    public void run() {
        for(int i = 0; i < 1000; i++) {
            System.out.println("yield..." + i);
        }
    }
}

4.5 线程运行

​ 当调用start方法,线程处于就绪状态时,如果cpu调度到了该线程,则该线程进入运行状态。

4.6 线程停止

修饰词和类型 方法名 描述
void stop 已弃用
这种方法本质上是不安全的。使用Thread.stop体质线程可以解锁所有已锁定的监视器(由于未ThreadDeath的自然结果)。如果先前受这些监视器保护的任何对象处于不一致的状态,则所怀的对象将变得对其他线程可见,可能导致任意行为。**stop许多用途应该被替换为只是修改一些变量以指示目标应该定期检查此变量,如果变量表示要停止运行,则以有序方式从其运行方法返回。**如果目标线程长时间(例如,在interrupt变量上),则应该使用interrupt方法来中断等待。有关详细信息,请参阅Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?。

​ 这句话的意思就是,在我们的线程体中,我们的循环加一个变量控制,有了这个变量,我们的外部就可以通过这个变量来终止这个线程的运行。

  • 不使用JDK提供的 stop()/destroy)() 方法 ( 他们本身也被JDK废弃了 )。
  • 提供一个boolean型的终止变量,当这个变量置位false,则终止线程的运行。
class Study implements Runnable {
    // 1. 线程类中 定义 线程体使用的标志
    private boolean flag = true;
    @Override
    public void run() {
        2. 线程体使用该标志
            while (flag) {
                System.out.println("study thread...");
            }
    }
    // 3. 对外提供方法改变标志
    public void stop() {
        this.flag = false;
    }
}

​ 以下是一个简单的线程控制的例子:

/**
 * 终止线程的两种方式:
 * 1. 线程正常执行完毕 --> 次数
 * 2. 外部干涉 -- > 加入标志位
 * 不要使用stop和destroy方法
 */
public class TerminateThread implements Runnable {

    // 1. 放入标志,标记线程体是否可以运行
    private boolean flag = true;
    private String name;

    public TerminateThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        // 2. 关联标志,true -- > 运行 false -- > 停止
        int i = 0;
        while (flag) {
            System.out.println(name + "--- >" + i++);
        }
    }

    // 3. 对外提供方法,改变标识
    public void terminate() {
        this.flag = false;
    }

    public static void main(String[] args) {
        TerminateThread tt = new TerminateThread("c罗");
        new Thread(tt).start();

        for(int i = 0; i < 10; i++) {
            if (i == 8) {
                tt.terminate();// 线程终止
                System.out.println("游戏结束");
            }
            System.out.println("main -- >" + i);
        }
    }
/* 运行结果是:
main -- >0
c罗--- >0
c罗--- >1
main -- >1
main -- >2
main -- >3
main -- >4
main -- >5
main -- >6
main -- >7
c罗--- >2
游戏结束
main -- >8
main -- >9
*/

4.7 阻塞状态

4.7.1 sleep方法

  • sleep( 时间 )指定当前的线程阻塞的毫秒数;
  • sleep存在异常InterruptedException;(本身重写Thread中的run方法和实现Runnable接口,都不能对外抛出异常,这个时候我们只能使用try-catch)
  • sleep时间达到后,线程进入就绪状态;
  • sleep可以模拟网络延时、倒计时等。
  • 对一个对象都有一个所,sleep不会释放锁;(就是说,sleep并不会释放资源)
修饰词和类型 方法名 描述
static void sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
static void sleep(long millis, int nanos) 导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。
具体的使用方式
class Web12306 implements Runnable {
    private int num = 50;
    public void run() {
        while (true) {
            if (num <= 0) {
                break;
            }
            try {
                Thread.sleep(500);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢到了" + num--);
        }
    }
}
使用的场景:
用来模拟网络延时–放大问题:
/**
 * sleep 模拟网络延时, 就放大了问题的可能性
 */

public class BlockedSleep01 implements Runnable{
    // 票数
    private int ticketNums = 10;
    @Override
    public void run() {
        // TODO Auto-generated method stub
        while(true) {
            if (ticketNums < 0) {
                break;
            }
            // 模拟网络延时
            try {
                Thread.sleep(200);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread() .getName() + "--->" + ticketNums --);
        }
    }
    public static void main(String[] args) {
        // 一份资源
        BlockedSleep01 web = new BlockedSleep01();
        // 多个代理
        new Thread(web, "黄牛1").start();
        new Thread(web, "黄牛2").start();
        new Thread(web, "黄牛3").start();
    }
}
/** 执行结果:
黄牛2--->10
黄牛3--->8
黄牛1--->9
黄牛2--->7
黄牛1--->6
黄牛3--->5
黄牛2--->4
黄牛3--->2
黄牛1--->3
黄牛2--->1
黄牛1--->-1
黄牛3--->0
*/
模拟休息:
/**
 * 模拟休息
 */


public class BlockedSleep02 implements Runnable{
    // 胜利者
    private static String winner;
    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int steps = 1; steps <= 30; steps ++) {
            // 休息
            if (Thread.currentThread().getName().equals("rabbit") && (steps % 10 == 0)) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "--->" + steps);
            // 是否结束
            boolean flag = gameOver(steps);
            if (flag) {
                break;
            }
        }
    }

    private boolean gameOver(int steps) {
        if (winner != null) {
            return true; // 存在胜利者
        }else {
            if (steps == 30) {
                winner = Thread.currentThread().getName();
                System.out.println("winner ==>" + winner);
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        BlockedSleep02 racer = new BlockedSleep02();
        new Thread(racer, "tortoise").start();
        new Thread(racer, "rabbit").start();
    }

}
/** 运行结果(由于使用线程,所以结果不固定):
tortoise--->1
rabbit--->1
tortoise--->2
rabbit--->2
tortoise--->3
rabbit--->3
rabbit--->4
rabbit--->5
rabbit--->6
rabbit--->7
rabbit--->8
rabbit--->9
tortoise--->4
tortoise--->5
tortoise--->6
tortoise--->7
tortoise--->8
tortoise--->9
tortoise--->10
tortoise--->11
tortoise--->12
tortoise--->13
tortoise--->14
tortoise--->15
tortoise--->16
tortoise--->17
tortoise--->18
tortoise--->19
tortoise--->20
tortoise--->21
tortoise--->22
tortoise--->23
tortoise--->24
tortoise--->25
tortoise--->26
tortoise--->27
tortoise--->28
tortoise--->29
tortoise--->30
winner ==>tortoise
rabbit--->10
*/
模拟倒计时:
import java.text.SimpleDateFormat;
import java.util.Date;

public class BlockedSleep03 {

    public static void main(String[] args) {
        // 倒计时
        Date endTime = new Date(System.currentTimeMillis() + 1000*10);
        long end = endTime.getTime();
        while (true) {
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("mm:ss").format(endTime));
                endTime = new Date(endTime.getTime() - 1000);
                if (end - 10000 > endTime.getTime()) {
                    break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void test() {
        // 倒数10个数,1秒一个
        int num = 10;
        while (true) {
            try {
                Thread.sleep(1000);
                System.out.println(num--);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
/** 每10秒弹出一个
46:44
46:43
46:42
46:41
46:40
46:39
46:38
46:37
46:36
46:35
46:34

Process finished with exit code 0
**/

4.7.2 Join方法

  • join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞。

​ 实际进行理解的时候,可以理解为插队线程。必须等待join线程执行完毕,后面的线程才能实行。join方法和sleep不同的是,join是成员方法,sleep是静态方法。join必须通过线程对象才能使用。你写在那个线程体中,哪个线程就被阻塞。

​ 就和汽车插队一样:当一辆汽车在绿灯前插队,后面的汽车必须等待前面一辆汽插队的车走完,才能走。

修饰词和类型 方法名 描述
void join() 等待这个线程死亡。
void join(long millis) 等待这个线程死亡最多million毫秒。
void join(long millis, int nanos) 等待最多million毫秒加上nanos纳秒这个线程死亡。

以下是一个例子:

/**
 * join 合并线程 插队线程
 */

public class BlockedJoin01 {

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("lambda...." +i);
            }
        });
        t.start();

        for (int i = 0; i < 100; i++) {
            if (i == 20) {
                try {
                    t.join(); // 插队 main被阻塞
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("main..." + i);
        }
    }
}

接下来是一个更好玩的故事:

/**
 * 老爸想抽烟的故事
 */

public class BlockedJoin02 {

    public static void main(String[] args) {
        System.out.println("爸爸和儿子买烟的故事");
        new Thread(new Father()).start();
    }

}

class Father extends Thread {
    @Override
    public void run() {
        System.out.println("老爸想抽烟,发现没了");
        System.out.println("让儿子去买中华");
        Thread t = new Thread(new Son());
        t.start();
        try {
            t.join(); // 老爸被阻塞
            System.out.println("老爸接过烟,把零钱给儿子");
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("孩子走丢了,老爸找孩子去了。。。。");
        }
    }
}

class Son extends Thread {
    @Override
    public void run() {
        System.out.println("接过老爸的钱,出发了");
        System.out.println("路边有个游戏厅,看了10秒");
        for(int i = 1; i <= 10; i++) {
            System.out.println(i + "秒过去了...");
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("想起来买烟,赶紧买烟");
        System.out.println("手拿一包中华,回家");
    }
}
/** 执行结果:
爸爸和儿子买烟的故事
想抽烟,发现没了
让儿子去买中华
接过老爸的钱,出发了
路边有个游戏厅,看了10秒
1秒过去了...
2秒过去了...
3秒过去了...
4秒过去了...
5秒过去了...
6秒过去了...
7秒过去了...
8秒过去了...
9秒过去了...
10秒过去了...
想起来买烟,赶紧买烟
手拿一包中华,回家
老爸接过烟,把零钱给儿子
**/

4.7.3 深度观察线程的状态

public class AllState {

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("...");
            }
        });
        // 观察状态
        Thread.State state = t.getState();
        System.out.println(state); // NEW

        t.start();
        state = t.getState();
        System.out.println(state); // RUNNABLE ( 具备线程能力或者运行中 )

        while (state != Thread.State.TERMINATED) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            state = t.getState(); // TIMED_WAITING
            System.out.println(state);
        }
        state = t.getState(); // TERMINATED
        System.out.println(state);
    }
}
/**结果:
NEW
RUNNABLE
RUNNABLE
...
...
RUNNABLE
...
RUNNABLE
...
RUNNABLE
...
RUNNABLE
TERMINATED
TERMINATED
**/

4.8 线程优先级

​ Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有编程。线程调度器按照线程的优先级决定应调度那个线程来执行。

​ 线程的优先级用数字表示,范围从1到10

  • Thread.MIN_PRIORITY = 1
  • Thread.MAX_PRIORITY = 2
  • Thread.NORM_PRIORITY = 3

​ 使用下述方法获得或者设置线程对象的优先级

  • int getPriority();
  • void setPriority(int newPriority);

优先级的设定建议在start()调用前

注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高后调用优先级低的进程。

以下是一个小小的例子:

/**
 * 线程的优先级 1-10
 * 1. NORM_PRIORITY 5
 * 2. MAX_PRIORITY 10
 * 3. MIN_PRIORITY 1
 * 概率,不代表绝对的先后顺序
 */
public class PriorityTest {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getPriority());

        MyPriority mp = new MyPriority();
        Thread t1 =new Thread(mp, "线程1");
        Thread t2 =new Thread(mp, "线程2");
        Thread t3 =new Thread(mp, "线程3");
        Thread t4 =new Thread(mp, "线程4");
        Thread t5 =new Thread(mp, "线程5");
        Thread t6 =new Thread(mp, "线程6");
        // 设置优先级在启动之前
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        t3.setPriority(Thread.MAX_PRIORITY);
        t4.setPriority(Thread.MIN_PRIORITY);
        t5.setPriority(Thread.MIN_PRIORITY);
        t6.setPriority(Thread.MIN_PRIORITY);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
    }

}

class MyPriority implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " " + Thread.currentThread().getPriority());
        Thread.yield();
    }
}
/**结果:
5
线程2 10
线程4 1
线程3 10
线程1 10
线程5 1
线程6 1
**/

4.9 线程的分类

  • 在jvm中,jvm将进程分为守护进程(后台进程)和用户进程;
  • 虚拟机必须确保用户进程执行完毕;
  • 虚拟机不必等待守护进程执行完毕;
  • 入后台记录日志、监控内存使用等;

4.9.1 守护进程(daemon)

  • 守护线程:是为用户线程服务的,jvm停止不用等待守护进程执行完毕。
  • 默认:用户线程 jvm等待所有的用户线程执行完毕。
  • 必须在线程执行前,将线程设置为守护线程。

以下是一些代码片段:

/**
 * 守护线程:是为用户线程服务的,jvm停止不用等待守护进程执行完毕
 * 默认:用户线程 jvm等待所有的用户线程执行完毕
 */

public class DaemonTest {

    public static void main(String[] args) {
        God god = new God();
        You you = new You();
        Thread t =  new Thread(god);
        t.setDaemon(true); // 将用户线程设置为守护线程
        t.start();
        new Thread(you).start();
    }
    
}

class You implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 365*100; i++) {
            System.out.println("happy Life");
        }
        System.out.println("ooooo......");
    }
}

class God extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("bless you...");
        }
    }
}

4.10 常用方法

修饰词和类型 方法名 描述
boolean isAlive() 用来测试这个线程是否还活着。
String getName() 返回此线程的名称。
void setName() 将此线程的名称更改为等于参数name。
static Thread currentThread() 返回对当前正在执行的线程对象的引用。

以下这些方法的使用:

/**
 * 其他方法
 */
public class InfoTest {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().isAlive());

        // 设置名称: 真实角色 + 代理角色
        MyInfo info = new MyInfo("战斗机");
        Thread t = new Thread(info);
        t.setName("公鸡中的战斗机"); // 这个是代理角色的名称
        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(t.isAlive());
    }

}

class MyInfo implements Runnable {

    private String name;

    public MyInfo(String name) {
        this.name = name; // 真实角色的名称要通过面向对象的思想来设置。
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

5. 线程的同步-并发控制

5.1 引入

​ 这里有一个苹果,一个人吃,怎样都不会出问题。但是两个人一起吃,可能就会出现咬到咬到对方的嘴的情况。出现这种情况,就需要我们去进行控制,好避免这种情况。多线程应用也是,必须有并发控制,否则就会出现线程之间抢占资源的情况。

5.2 概念

​ 线程同步,是指在一个多线程环境下,我们要保证数据的正确性和安全性。同时我们必须保证效率。

​ 线程的同步为什么比较难呢?

  • 第一点,因为线程同步不形象。
  • 第二点,在保证数据的正确性和准确性的同时,必须保证程序性能。这个就是并发控制,是非常难的。

​ 在java中,并发操作使用synchronized来实现,代码上并不难,但是需要考究算法。

​ 并发:同一对象多个线程同时操作。

​ 比如,同时操作同一个账户,同时购买同一车次的票

5.3 非同步的三大经典案例

​ 接下来我们来看看代码。

​ 首先是,没有并发控制的代码,还是之前的例子:

/**
 * 线程不安全 数据有负数 或者 相同的情况
 */

public class UnsafeTest01 {

    public static void main(String[] args) {
        // 一份资源
        UnsafeWeb12306 web = new UnsafeWeb12306();
        // 多个代理
        new Thread(web, "黄牛1").start();
        new Thread(web, "黄牛2").start();
        new Thread(web, "黄牛3").start();
    }

}

/**
 * 模拟线程不安全
 */
class UnsafeWeb12306 implements Runnable{

    private boolean flag = true;

    // 票数
    private int ticketNums = 10;
    @Override
    public void run() {
        while(flag) {
            test();
        }
    }

    public void test() {
        if (ticketNums < 0) {
            flag = false;
            return ;
        }

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "--- >" + ticketNums--);
    }

}
/** 结果:
黄牛2--- >10
黄牛3--- >10
黄牛1--- >9
黄牛2--- >8
黄牛3--- >8
黄牛1--- >7
黄牛2--- >6
黄牛3--- >4
黄牛1--- >5
黄牛2--- >3
黄牛1--- >2
黄牛3--- >2
黄牛2--- >1
黄牛3--- >-1
黄牛1--- >0
**/

​ 我们发现,这些代码运行的结果会出现两种情况:

​ 一种是不同的黄牛拿了一张票,一种是余票出现了负数,这时为什么呢?

​ 原因如下:

尚学堂视频笔记六:多线程_第4张图片

  1. 余票为负。这时因为3个线程同时对最后一张票进行申请,但是A在拿到票之前需要等待200毫秒,**但是,A抱着这个最后一张票的资源。**但是,此时,B,C也等待200毫秒,此时,他们也抱着最后一张票的资源在睡觉(sleep)。所以,当"睡觉"结束,A取得票1,之后B,C相继“醒来”,然后C拿到0, B拿到-1。(结果不唯一,要看cpu的调度情况)

尚学堂视频笔记六:多线程_第5张图片

  1. 出现两个或以上的相同的票。这个是由于每个线程都有自己的工作内存。而票数,ticketNums 是存在主内存中的。**每次,A,B,C这三个线程都会把ticketNums拷进自己的工作内存,然后再和主内存进行交互。**此时,有可能是A,先将10张票拷进自己的工作内存,但是还没有来得及和主内存进行交互,B就已经将第10张票拷进了自己的工作内存中,此时,就出现了A拿到了第10张票, B也拿到了第10张票;

再来看一个例子,这个例子中,我们才用了if来进行判断

/**
 * 线程不安全:取钱
 */

public class UnsafeTest02 {

    public static void main(String[] args) {
        // 账户
        Account account = new Account(1000000, "结婚礼金");
        Drawing you = new Drawing(account, 200000, "可悲的你");
        Drawing wife = new Drawing(account, 900000, "开心的她");
        you.start();
        wife.start();
    }

}

class Account {
    int money; // 金额
    String name; // 名称

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

class Drawing extends Thread {

    Account account; // 取钱的账户
    int drawingMoney; // 取的钱数
    int packetTotal; // 口袋的总数

    public Drawing(Account account, int drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        if(account.money - drawingMoney < 0) {
            return ;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.money -= drawingMoney;
        packetTotal += drawingMoney;
        System.out.println(this.getName() + "-- > 账户余额为 :" + account.money );
        System.out.println(this.getName() + "-- > 口袋的钱为 :" + this.packetTotal);
    }
}

以下为结果:

/**
可悲的你-- > 账户余额为 :-100000
可悲的你-- > 口袋的钱为 :200000
开心的她-- > 账户余额为 :-100000
开心的她-- > 口袋的钱为 :900000
**/

​ 结果我们发现,我们使用的if判断,根本没有什么用处,所以这种方法行不通。我们需要安全锁。

我们再来看一例:

​ 在我们操作容器的时候,我们不停地想容器中放入数据,如果容器不控制,他会有丢失和覆盖的情况。

import java.util.ArrayList;
import java.util.List;

/**
 * 线程不安全:操作容器
 */
public class UnsafeTest03 {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        System.out.println(list.size());
    }
    
}

最后的结果为: 9815。

​ 很明显,有些数据被覆盖或者丢失掉了。以上就是我们操作多线程但是不控制并发的典型案例。

​ 线程不安全这类错误在我开发的后期随处可见。这里提一下,并不是所有的多线程=操作我们都需要进行并发控制。如果我们操作数据,需要对数据进行改动,需要并发控制;但是如果我们是读取数据,那么没有关系。

5.5 多线程并发与同步 队列与锁

​ 并发之前提过,就是同一个对象被多个线程同时操作。

​ 这里有3个要点:同一个对象;多个线程;同时操作;

​ 一旦形成并发,就可以形成数据不准确的问题。如何解决这些问题?显然,排队。

​ 比如,一个教室里只有一台电脑,但是有多个学生,如何去合理的规划使用这台电脑?排队。我们先将学生排好队,然后根据一定的算法,去决定谁先用谁后用。这样,还是同一时间,一个学生,操作这一台电脑。

尚学堂视频笔记六:多线程_第6张图片

​ 那么怎么知道1号线程使用好了,1号线程在使用呢?

​ 我们知道,我们住酒店,前台会给你一张卡,你拿着这张卡,说明这个房间你在使用,其他人无法使用这个房子;你将这张卡退给前台,前台就明白,你退房了,你不在使用了。这是我们现实中的做法。

​ 那么在jvm中,这张卡就改成了对象的排他锁。对象内部的锁实际上是一种标志,表示了这个资源被占用了还是没有被占用。所以我们拿这个锁就行了。

​ 一个队列,一个排他锁,这个就是多线程编程中的同步。

5.5.1 线程同步

​ 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这个时候,我们就需要用到"线程同步"。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

​ 每一线程都有一个,当在等待队列中时,线程会进行斗争,获得资源的线程会将资源锁定,而其他的线程只有等待。

​ 由于统一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronize),当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题:

  • 一个线程持有锁会导致其它所有需要此锁的线程挂起
  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。

尚学堂视频笔记六:多线程_第7张图片

​ 由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized 方法和synchronized 块。

  • 同步方法

​ public synchronized void method(int args) {}

​ synchronized 方法控制对 “成员变量|类变量”对象的访问:每个对象对应一把锁,每个synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后,被阻塞的线程方能获得该锁,重新进入可执行状态。

​ 缺陷:若将一个大的方法声明为 synchronized 将会大大影响效率。

接下来我们来看代码:

​ 第一段代码,通过修改之前买票的例子:

/**
 * 线程安全: 在并发时保证数据的正确性,效率尽可能高
 * synchronized
 * 1. 同步方法
 * 2. 同步块
 */

public class SynTest01 {

    public static void main(String[] args) {
        SafeWeb12306 web = new SafeWeb12306();

        new Thread(web, "黄牛1").start();
        new Thread(web, "黄牛2").start();
        new Thread(web, "黄牛3").start();
    }

}

class SafeWeb12306 implements Runnable {

    private int ticketNums = 10;
    private boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            test();
        }
    }

    // 线程安全 同步
    public synchronized void test() {

        if (ticketNums < 1) {
            flag = false;
            return;
        }

        System.out.println(Thread.currentThread().getName() + "-- > " + ticketNums--);

    }
}

​ 以下是结果:

/**
黄牛1-- > 10
黄牛3-- > 9
黄牛2-- > 8
黄牛2-- > 7
黄牛1-- > 6
黄牛3-- > 5
黄牛3-- > 4
黄牛1-- > 3
黄牛2-- > 2
黄牛3-- > 1
**/

​ 这里我们看到,我们使用synchronized锁住的,是 test 这个方法。然而,当我们仔细去看的时候,我们发现,test 方法中操作的变量是 ticket 和 flag 这两个成员变量。而 这个类中,除了重写的 run 方法外,其他资源都被锁住了。所以,synchronized 这个关键字,实际上锁住的是 web 这个对象中的所有资源。这里强调,锁住的是资源,是对象的资源。

​ 我们接下来再看一个例子,这个例子是之前的提款机取钱的例子

public class SynTest02 {

    public static void main(String[] args) {
        Account account = new Account(1000000, "结婚礼金");
        SafeDrawing you = new SafeDrawing(account, 200000, "可悲的你");
        SafeDrawing wife = new SafeDrawing(account, 900000, "开心的她");
        you.start();
        wife.start();
    }

}

class SafeDrawing extends Thread {
    Account account;
    int drawingMoney;
    int packetTotal;

    public SafeDrawing(Account account, int drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        test();
    }
	
    // 目标不对锁定失败,这里不是锁this,而是account对象。
    public synchronized void test() {
        if (account.money - drawingMoney < 0) {
            return ;
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        account.money -= this.drawingMoney;
        this.packetTotal += this.drawingMoney;

        System.out.println(this.getName() + "--- > 账户余额为:" + account.money);
        System.out.println(this.getName() + "--- > 口袋的钱为:" + packetTotal );
    }
}

​ 以下为结果:

/**
可悲的你--- > 账户余额为:800000
可悲的你--- > 口袋的钱为:200000
开心的她--- > 账户余额为:-100000
开心的她--- > 口袋的钱为:900000
**/

​ 这时,我们发现,我们将方法封装,并在前面加上synchronized修饰词,并没有起到锁住资源的作用。这时因为,账户余额,在account类中,并不在我们写的SafeDrawing类中。而我们只是锁住了SafeDrawing类中的属性。

​ 所以我们应该使用同步块来锁定account对象。

5.5.2 同步块

  • 同步块:synchroinzed (obj) {},称之为同步监视器

    • obj可以是任何对象,但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this即该对象本身,或class即类的模子
  • 同步监视器的执行过程

    • 第一个线程访问,锁定同步监视器,执行其中代码
    • 第二个线程访问,发现同步监视器锁定,无法访问
    • 第一个线程访问完毕,解锁同步监视器
    • 第二线程访问,发现同步监视器未锁,锁定并访问
  • java中一共有4中块:

    • 第一种,方法中的块,成为局部块,用来解决局部变量的作用域。比如在该块中定义了一个整型变量 a , 那么这个 a 只能在块中使用,出来块就不行了。

    • 如果在类中,和类中的方法处于同一个位置,我们称这个块为构造块,初始化变量信息。

    • 如果在构造块前面加一个static,这个称为静态块。静态块是用来初始化类的信息,静态块现有构造块执行。

    • 同步块,在方法中,用来解决线程安全的问题。

    这个是改进后的取款的代码:

/**
同步块, 目标更明确
**/
public class SynTest02 {

    public static void main(String[] args) {
        Account account = new Account(1000000, "结婚礼金");
        SafeDrawing you = new SafeDrawing(account, 200000, "可悲的你");
        SafeDrawing wife = new SafeDrawing(account, 900000, "开心的她");
        you.start();
        wife.start();
    }

}

// 线程安全
class SafeDrawing extends Thread {
    Account account;
    int drawingMoney;
    int packetTotal;

    public SafeDrawing(Account account, int drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        test();
    }

    public void test() {

        synchronized (account) {

            if (account.money - drawingMoney < 0) {
                return;
            }

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            account.money -= this.drawingMoney;
            this.packetTotal += this.drawingMoney;

            System.out.println(this.getName() + "--- > 账户余额为:" + account.money);
            System.out.println(this.getName() + "--- > 口袋的钱为:" + packetTotal);

        }
    }
}

​ 这里我们加了一个同步块,用来监视account属性,只有当account属性没有在使用时,其他的线程才能使用。

​ 以下是结果:

/**
可悲的你--- > 账户余额为:800000
可悲的你--- > 口袋的钱为:200000
**/

​ 发现结果正确。

​ 实际上我们还可以提高它的性能。如果,账户中没有钱了,那么当多线程过来运算,每一个线程都必须等前一个线程处理完毕才能让下一个线程进入。实际上没有必要。怎么办?加一个判断。

public void test() {
    // 提高性能
    if (account.money <= 0 ) {
		return ;
    }
    
    synchronized (account) {
        if (account.money - drawingMoney < 0) {
            return ;
        }
        // ... 以下省略
} 

​ 大家不要小看这些代码,这些代码才是在以后的工作中,能确实提高我们程序并发量的代码。这样的代码非常值钱。

​ 那么非同步的三大典型例子中的,操作容器的例子如何添加锁呢?

​ 我们发现,想容器中添加对象的动作是靠lambda式子完成,没有方法,那我们就不能直接在方法修饰词前添加synchronized。经过分析,发现只要锁住了list这个容器就可以了。所以将 list.add() 方法添加到 synchronized 块中:


    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list) {
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(list.size());
    }

​ 由于我们线程是按顺序向容器中添加对象的,所以有可能全部的线程还没有添加完成,下面的输出语句就已经输出了,所以我们需要添加一个延迟进行等待。当等待所有线程添加完毕后,再输出,最后结果为:

// 10000

5.5.3 并发的几个经典例子

电影院购票:

import java.util.ArrayList;
import java.util.List;

/**
 * 快乐影院
 */

public class HappyCinema {

    public static void main(String[] args) {
        // 可用位置
        List<Integer> available = new ArrayList<>();
        available.add(1);
        available.add(3);
        available.add(5);
        available.add(6);
        available.add(7);
        List<Integer> seats1 = new ArrayList<>();
        List<Integer> seats2 = new ArrayList<>();
        seats1.add(1);
        seats1.add(2);
        seats2.add(3);
        HighCinema cinema = new HighCinema(available, "Happy");
        new Thread(new HappyCustomer(cinema, seats1), "1").start();
        new Thread(new HappyCustomer(cinema, seats2), "2").start();
    }

}

class HappyCustomer implements Runnable {

    HighCinema cinema;
    List<Integer> seats;

    public HappyCustomer(HighCinema cinema, List<Integer> seats) {
        this.cinema = cinema;
        this.seats = seats;
    }

    @Override
    public void run() {
        synchronized (cinema) {
            boolean flag = cinema.bookTickets(seats);
            if (flag) {
                System.out.println("出票成功 " + Thread.currentThread().getName() + " -< 位置为: " + seats);
            } else {
                System.out.println("出票失败 " + Thread.currentThread().getName() + " -< 位置不够");
            }
        }
    }
}

class HighCinema {
    private List<Integer> available; // 可用位置
    String name; // 名称

    public HighCinema(List<Integer> available, String name) {
        this.available = available;
        this.name = name;
    }

    //购票
    public boolean bookTickets(List<Integer> seats) {
        System.out.println("可用位置为: " + available);
        List<Integer> copy = new ArrayList<>();
        copy.addAll(available);
        // 相减
        copy.removeAll(seats);
        // 判断大小
        if (available.size() - copy.size() != seats.size()) {
            return false;
        }
        // 成功
        available = copy;
        return true;
    }

}

​ 购买火车票的例子,和上面的例子相似,但是我们在锁资源的时候采用其他方式。

/**
 * 火车票
 */

public class Happy12306 {


    public static void main(String[] args) {
        Web12306 c = new Web12306(4, "happy sxt");
        new Passenger(c, "老高", 2).start();
        new Passenger(c, "老裴", 1).start();
    }

}

//顾客
class Passenger extends Thread {
    int seats;

    public Passenger(Runnable target, String name, int seats) {
        super(target, name);
        this.seats = seats;
    }

}

//火车票网
class Web12306 implements Runnable {
    int available; //可用的位置
    String name; //名称

    public Web12306(int available, String name) {
        this.available = available;
        this.name = name;
    }

    public void run() {
        Passenger p = (Passenger) Thread.currentThread();
        boolean flag = this.bookTickets(p.seats);
        if (flag) {
            System.out.println("出票成功" + Thread.currentThread().getName() + "-<位置为:" + p.seats);
        } else {
            System.out.println("出票失败" + Thread.currentThread().getName() + "-<位置不够");
        }
    }

    //购票
    public synchronized boolean bookTickets(int seats) {
        System.out.println("可用位置为:" + available);
        if (seats > available) {
            return false;
        }
        available -= seats;
        return true;
    }
}

​ 对于容器的线程安全的操作,在Java util 中 的concurrent包中的CopyOnWriteArrayList类为我们提供了线程安全的方法:


import java.util.concurrent.CopyOnWriteArrayList;

public class SynContainer {

    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(list.size());
    }

}

5.6 死锁-产生和解决

5.6.1 概念

​ 死锁:多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情况。某一个同步块同时拥有"两个以上对象的锁"时,就可能会发生"死锁"的问题。

5.6.2 例子和讲解

​ 下面来看一个两个女孩化妆的故事,这个故事和哲学家吃饭的问题类似:

/**
 * 死锁:过多的同步可能造成互相不释放资源,从而互相等待,一般发生于同步中
 * 持有多个对象的锁。
 */

public class DeadLock {

    public static void main(String[] args) {
        MakeUp girl1 = new MakeUp(1, "大丫");
        MakeUp girl2 = new MakeUp(0, "小丫");
        girl1.start();
        girl2.start();
    }

}

// 口红对象
class Lipstick {

}

class Mirror {

}

// 化妆
class MakeUp extends Thread {
    // 一份对象
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    // 选择
    int choice;
    // 名字
    String girlName;
    public MakeUp(int choice, String girlName) {
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        // 化妆
        makeup();
    }
    // 相互持有对方的对象锁 -- > 可能造成死锁
    public void makeup() {
        if(choice == 0) {
            synchronized (lipstick) { // 获得口红的锁
                System.out.println(this.girlName + " 获得口红");
                // 一秒后想拥有镜子锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (mirror) {
                    System.out.println(this.girlName + " 获得镜子");
                }
            }
        }else {
            synchronized (mirror) { // 获得镜子的锁
                System.out.println(this.girlName + " 获得镜子");
                // 二秒后想拥有口红锁
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipstick) {
                    System.out.println(this.girlName + " 获得口红");
                }
            }
        }
    }

}

​ 这里为什么会造成死锁呢?

​ 首先,我们先假设是女孩1首先拿起了镜子,等待2秒钟。在等待的过程中,女孩2拿起了口红,并等待1秒钟。这个时候,女孩1拥有镜子,等待口红的资源,才能继续运行。而女孩2拥有口红,等待镜子才能继续化妆。这样,就形成了死锁。

​ 那么死锁怎么解决?

​ 无非就是保证对象不要相互持有对方的锁就可以了。

​ 所以,这个问题就很好解决,我们只要将其中的一个synchronized块移出synchronized块,不要形成嵌套锁就可以了。


    // 相互持有对方的对象锁 -- > 可能造成死锁
    public void makeup() {
        if(choice == 0) {
            synchronized (lipstick) { // 获得口红的锁
                System.out.println(this.girlName + " 获得口红");
                // 一秒后想拥有镜子锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 将该同步块移出,取消嵌套锁即可
            synchronized (mirror) {
                System.out.println(this.girlName + " 获得镜子");
            }
        }else {
            synchronized (mirror) { // 获得口红的锁
                System.out.println(this.girlName + " 获得镜子");
                // 二秒后想拥有镜子锁
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (lipstick) {
                System.out.println(this.girlName + " 获得口红");
            }   
        }
    }

}

5.7线程协作(cooperation)-生产者消费者模式

5.7.1 线程通信

​ 应用场景:生产者和消费者问题

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入长裤,消费者将仓库中产品取走消费;
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止;
  • 如果仓库中存放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。

尚学堂视频笔记六:多线程_第8张图片

​ 所以,仓库才是二者之间交互的中间人。

​ 分析:这是一个线程同步的问题,生产者和消费者共享同一个资源,并且生产者和消费者之间互相依赖,互为条件。

  • 对于生产者,没有生产产品之前,要通知消费者等待。而生产了产品之后,有需要马上通知消费者消费;
  • 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费;
  • 在生产者消费者问题中,仅有 synchronized 是不够的
    • synchronized 可阻止并发更新同一个共享资源,实现了同步
    • synchronized 不能用来实现不同线程之间的消息传递(通信)

​ 如何解决synchronized 不能解决通信的问题呢?

​ 解决方法一:并发协作模型 “生产者/消费者模式” - > 管程法 (解耦、提高效率)

  • 生产者:负责生产数据的模块 (这里的模块可能是:方法、对象、线程、进程);
  • 消费者:负责处理数据的模块 (这里模块可能是:方法、对象、线程、进程);
  • 缓冲区:消费者不能直接使用生产者的数据,它们之间有一个 “缓冲区”;生产者将生产好的数据放入 “缓冲区”, 消费者从 “缓冲区” 拿要处理的数据。

尚学堂视频笔记六:多线程_第9张图片

​ 解决方法二:并发协作模型 “生产者/消费者模式” - > 信号灯法

​ 模仿了显示生活中,人们过马路的情景:绿灯,行人停下,车辆通行。红灯,行人通行,车辆停下。所以,在我们实现 “生产者消费者模式” 时,我们可以设置信号,利用信号提示消费者或者生产者开始工作

​ Java中提供了3个方法解决线程之间的通信问题

方法名 作用
final void wait(0) 表示线程一直等待,知道其他线程通知,与 sleep 不同,会释放锁
final void wait (long timeout) 指定等待的毫秒数
final void notify() 唤醒一个处于等待状态的线程 (等待队列的第一个线程)
final void notifyAll() 唤醒同一个对象上的所有调用wait()方法的线程,线程优先级高的线程优先调用

​ 均是java.lang.Object类的方法,都只能在同步方法或者同步块代码中使用,否则抛出异常

生命周期

尚学堂视频笔记六:多线程_第10张图片

​ wait同样是阻塞的一种,但是会释放锁。当对方调用了notify 或者 notifyAll方法,将会被重新唤醒。

例子
管程法例子:
/**
 * 协作模型:生产者消费者实现方式一:管程法
 */

public class CoTest01 {

    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Consumer(container).start();
    }

}

// 生产者
class Productor extends Thread {
    SynContainer container;

    public Productor(SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        // 开始生产
        for (int i = 0; i < 100; i++) {
            System.out.println("生产 -- >" + i + "第几个馒头");
            container.push(new SteamedBun(i));
        }
    }
}

// 消费者
class Consumer extends Thread {
    SynContainer container;

    public Consumer(SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        // 开始消费
        for (int i = 0; i < 100; i++) {
            System.out.println("消费 -- >" + container.pop().id + "个馒头");
        }
    }
}

// 缓冲区
class SynContainer {
    SteamedBun[] buns = new SteamedBun[10];
    int count = 0; // 计数器

    // 存储 生产
    public synchronized void push(SteamedBun bun) {
        // 何时能生产 容器存在空间
        // 不能生产 只有等待
        if (count == buns.length) {
            try {
                this.wait(); //线程阻塞  消费者通知生产
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 存在空间 可以生产
        buns[count] = bun;
        count++;
        // 存在数据可以通知消费
        this.notifyAll();
    }

    // 获取 消费
    public synchronized SteamedBun pop() {
        // 何时消费 容器中是否存在数据
        // 没有数据 只有等待
        if (count == 0) {
            try {
                this.wait(); // 线程阻塞 生产者通知消费, 解除阻塞
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 存在数据可以消费
        count--;
        SteamedBun bun = buns[count];
        this.notifyAll(); // 存在空间, 可以唤醒对方生产
        return bun;
    }
}

// 馒头
class SteamedBun {
    int id;

    public SteamedBun(int id) {
        this.id = id;
    }
}
信号灯例子
/**
 * 协作模型:生产者消费者实现方式二:信号灯法
 * 借助标志位
 */

public class CoTest02 {

    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

// 生产者 演员
class Player extends Thread {
    TV tv ;


    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.tv.play("奇葩说");
            } else {
                this.tv.play("太污了");
            }
        }
    }

}

// 消费者 观众
class Watcher extends Thread {
    TV tv;

    public Watcher(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            this.tv.watch();
        }
    }
}

// 同一个资源 电视
class TV {
    String voice;
    // 信号灯
    // T 表示演员表演 观众等待
    // F 表示观众观看 演员等待
    boolean flag = true;

    // 表演
    public synchronized void play(String voice) {
        // 演员等待
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("表演了:" + voice);
        this.voice = voice;
        // 唤醒
        this.notifyAll();
        this.flag = !this.flag;
    }

    // 观看
    public synchronized void watch() {
        // 观众等待
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 观看
        this.notifyAll();
        this.flag = !this.flag;
        System.out.println("听到了 : " + voice);
    }
}

6. 高级主题-更上一层楼!

6.1 任务定时调度

6.1.1 Timer 和 TimerTask

​ 说白了就是一个闹钟。

​ 例如,某一个有规律的时间点干什么事情:

  • 每天早上8点起床,闹钟响起
  • 每年4月1日给自己当年暗恋女神发一封匿名贺卡
  • 每隔一个小时,上传一下自己的学习笔记到云盘

​ 通过 Time 和 Timetask,我们可以实现定时启动某一个线程。

  • java.util.Timer:类似闹钟的功能,本身实现的就是一个线程
  • java.util.TimerTask:一个抽象类,该类实现了Runnable接口,所以该类具备了多线程的能力

​ 以下是示例代码:

class TestTimer {
    public static void main () {
        Timer t1 = new Timer(); // 定义计时器
        MyTask task1 = new Mytask();
        t.schedule(task1, 3000); // 3秒后执行 
        // t1.schedule(task1, 5000, 1000); 5秒以后每个1秒执行一次
        // GregorianCalendar calender1 = new GregorianCalendar(2010, 0, 5, 14, 36, 57);
        // t1.schedule(task1, calendar1.getTime()); 指定时间定时执行
    }
}
class MyTask extends TimerTask { // 自定义线程类继承TImerTask类
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Task1:" + i);
    }
}

​ 以下是一个小的代码:

import java.util.*;

/**
 * 任务调度: 借助Timer 和 TimerTask 类
 */

public class TestTask01 {

    public static void main(String[] args) {
        Timer timer = new Timer();
        // 执行安排
//        timer.schedule(new MyTask(), 1000); // 执行任务一次
//        timer.schedule(new MyTask(), 1000, 200); // 执行多次
        Calendar cal = new GregorianCalendar(2099, 12, 31,21,53,54);
        timer.schedule(new MyTask(), cal.getTime(), 200); // 执行多次
    }

}

class MyTask extends TimerTask {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("放空大脑休息一会");
        }
        System.out.println("结束");
    }
}

​ 我想这个我一辈子都等不到。

6.1.2 QUARTZ

​ 对于简单的时间调度,我们可以使用Timer和TimerTask。但是对于复杂的时间调度,我们可以使用QUARTZ (石英表)。这个框架已经集成到juc并发包

​ 这个框架由四大部分组成:

  • Scheduler - 调度器:控制所有的调度
  • Trigger - 触发条件,采用DSL模式
  • JobDetail - 需要处理的JOB
  • Job - 执行逻辑

​ 其中,DSL指:

​ Domain-specific language 领域特定语言,针对一个特定的领域,具有受限表达性的一种计算机程序语言,即领域专用语言,声明式编程:

  • Method Chaining 方法链 Fluent Style 流畅风格, builder模式构建器
  • Nested Functions 嵌套函数
  • Lambda Expressions/Closure
  • Function Sequence

​ 以下是其中的第一个例子:

/* 
 * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 
 * use this file except in compliance with the License. You may obtain a copy 
 * of the License at 
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0 
 *   
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 
 * License for the specific language governing permissions and limitations 
 * under the License.
 * 版权声明,看不看无所谓
 */ 
 
package com.thread.other;

import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

/**
 * 

* This is just a simple job that says "Hello" to the world. * 这是一个简单的向世界问好的job *

* * @author Bill Kratzer */
public class HelloJob implements Job { private static Logger _log = LoggerFactory.getLogger(HelloJob.class); /** *

* Empty constructor for job initilization * 空的构造声明 *

*

* Quartz requires a public empty constructor so that the * scheduler can instantiate the class whenever it needs. * Quartz 需要一个公用的空构造器,让调度程序可以在它想要的任何时候初始化一个类。 *

*/
public HelloJob() { } /** *

* Called by the {@link org.quartz.Scheduler} when a * {@link org.quartz.Trigger} fires that is associated * with * the Job. * 当与相关作业关联的 {@link org.quartz.Trigger} 触发时,由{@link * org.quartz.Trigger} 调用 *

* * @throws JobExecutionException * if there is an exception while executing the job. */
public void execute(JobExecutionContext context) throws JobExecutionException { // Say Hello to the World and display the date/time _log.info("Hello World! - " + new Date()); } } /* * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy * of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. * 版权声明可以不看 */ package com.thread.other; import static org.quartz.DateBuilder.evenMinuteDate; import static org.quartz.JobBuilder.newJob; import static org.quartz.TriggerBuilder.newTrigger; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerFactory; import org.quartz.Trigger; import org.quartz.impl.StdSchedulerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; /** * This Example will demonstrate how to start and shutdown the Quartz scheduler and how to schedule a job to run in * Quartz. * 这个例子将会论证如何开始和关闭一个Quartz调度程序,以及怎样安排一个作业在Quartz中运 * 行 * @author Bill Kratzer */ public class SimpleExample { public void run() throws Exception { Logger log = LoggerFactory.getLogger(SimpleExample.class); log.info("------- Initializing ----------------------"); // First we must get a reference to a scheduler // 首先 我们必须获得一个调度程序 (scheduler)的声明 SchedulerFactory sf = new StdSchedulerFactory(); Scheduler sched = sf.getScheduler(); log.info("------- Initialization Complete -----------"); // computer a time that is on the next round minute // 计算下一轮的时间 Date runTime = evenMinuteDate(new Date()); log.info("------- Scheduling Job -------------------"); // define the job and tie it to our HelloJob class // 定义作业并将其和我们的HelloJob类绑定 JobDetail job = newJob(HelloJob.class).withIdentity("job1", "group1").build(); // Trigger the job to run on the next round minute // 触发作业在下一轮分钟运行 Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime).build(); // Tell quartz to schedule the job using our trigger // 通知 quartz 使用我们的触发器来安排作业 sched.scheduleJob(job, trigger); log.info(job.getKey() + " will run at: " + runTime); // Start up the scheduler (nothing can actually run until the // scheduler has been started) // 启动调度程序 (没有实际的运行直到调度程序已经开始) sched.start(); log.info("------- Started Scheduler -----------------"); // wait long enough so that the scheduler as an opportunity to // run the job! // 等待一定的时间,一边调度程序有机会运行作业! log.info("------- Waiting 65 seconds... -------------"); try { // wait 65 seconds to show job // 等待65秒 Thread.sleep(65L * 1000L); // executing... // 执行中... } catch (Exception e) { // } // shut down the scheduler // 停止调度程序 log.info("------- Shutting Down ---------------------"); sched.shutdown(true); log.info("------- Shutdown Complete -----------------"); } public static void main(String[] args) throws Exception { SimpleExample example = new SimpleExample(); example.run(); } }

6.2 HappenBefore

6.2.1 引入

​ 你写的代码很可能根本没有按照你期望的驯顺执行。因为编译器和CPU会尝试重排指令时的代码更快地运行。一般发生在我们的代码相互和相互之间没有直接地联系,没有相互的依赖,cpu可能将后面的代码向前提。这个我们就叫做代码重排。

​ 我们编写的代码到后来会被编译成机器码,机器码会影响到我们那个变量用到那个存储器或是哪个寄存器。

我们一条指令执行的步骤:
  • 从内存中拿到一条指令;
  • 指令进行解码再从寄存器中拿值;
  • 开始计算结果;
  • 再将结果写回寄存器。

​ cpu在进行计算求值并写回时候,如果写回的时间比较长,CPU会查看下一条指令与当前指令有没有关系,如果没有关系,CPU可能将下一条指令提前到当前指令之前,进行运算。

​ 例如下面的代码:

subTotal = price + fee;
total += subTatal;
isDone = true;

​ 编译器将其翻译成汇编语言:

ADD R1, R2 -> R3
ADD R4, R3 -> R4
MOV 1 -> R5

​ 如果这个时候,在计算total时,所有的时间较长,而下面一条代码与前面的代码没有关系,此时CPU就有可能将第3条代码提前到第二条代码前进行执行。

​ 如果这些代码是在多线程的状态运行,isDone 可能已经执行,而另一条线程中total并没有计算出来,这样的程序运行出来的结果就和我们的设想出现了差距。

​ 所以,在多线程中,指令重排,就可能出现问题。

6.2.2 指令重排:

​ 执行代码的顺序可能与编写代码不一致,即虚拟机优化代码顺序,则为指令重排happen-before。即:编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。

  • 在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这些规则后面在叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而卸载前面的代码会后执行——以尽可能充分地利用CPU。那上面的例子来说,假如不是a = 1 的操作,而是a = new byte[1024*1024] (分配1M空间),那么它会运行地很慢,此时CPU是等待其执行结束呢,还是先执行下面那句flag = true 呢?显然,先执行flag = true 可以提前使用CPU,加快整体效率,当然这样的前提是不会产生错误(什么错误后面再说)。虽然这里有两种情况:后面的代码先于前面的代码开始执行;前面的代码先开始执行,但当效率较慢的时候,后面的代码开始执行并先于前面的代码执行结束。不管谁先开始,总之后面的代码在一些情况下存在先结束的可能。
  • 在硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。

6.2.3 数据依赖

​ 之前我们强调,指令重排发生在没有数据依赖的情况。在有数据依赖的情况下,编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

6.3.1 概念

​ 如果两个操作访问统一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分为下列三种类型:


名称 代码示例 说明
写后读 a = 1; b = a; 写一个变量之后,再读取这个位置。
写后写 a = 1; a = 2; 写一个变量之后,再写这个变量。
读后写 a = b; b = 1; 读一个变量之后,再写这个变量。

​ 上面三种情况,只要重新排序两个操作的执行顺序,程序的执行结果就会改变。

6.3 Volatile

​ volatile 保证线程间变量的可见性,简单地说就是当线程A对变量X进行修改后,在线程A后面执行的其他线程能看到变量X的变动,更详细地说要符合以下两个规则:

  • 线程对变量进行修改后,要立刻写回到主存。
  • 线程对变量读取的时候,要从主存中读取,而不是缓存。

尚学堂视频笔记六:多线程_第11张图片

​ 各线程的工作内存间彼此独立、互不可见,在线程启动的时候,虚拟机为每一个内存分配一块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本,即为了提高效率

volatile是不错的机制,但是volatile不能保证原子性。

​ 以下是例子:

/**
 * 用于保证数据的同步,也就是可见性
 */

public class VolatileTest {

    public volatile static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (num == 0) {// 此处不要编写代码

            }
        }).start();

        Thread.sleep(1000);
        num = 1;
    }

}
/**
* 结果就是 1 后停止
*/

​ 如果将volatile去掉,那么这个程序将永远不能停止。因为CPU一直忙于该线程的运行(死循环),并能很快的将num = 1 同步到该线程的工作内存中,所以一直循环。

​ 但是如果加了volatile,当num 发生改变时,主存立刻通知工作线程 num 发生改变。此时,工作内存立刻更新num 数值, 然后循环停止。

6.4 dcl单例模式-设计模式

​ dcl单例模式的目的是:对外只有一个对象,对内不管,创建创建多少个对象都无所谓。

​ 单例模式有很多类型:懒汉式、饿汉式、double-checking等等。这里主要讲解 double-checking。

​ 以下是一个例子:

/**
 * DCL单例模式:懒汉式套路基础上加入并发控制,保证多线程环境下,对外存在一个对象、
 * 1、 构造器私有化 -- > 避免外部创建对象
 * 2、 提供私有的静态属性 -- > 存储对象的地址
 * 3、 提供公共的静态方法 -- > 获取属性
 */

public class DoubleCheckedLocking {

    // 2、提供私有的静态属性
    private volatile static DoubleCheckedLocking instance;
    // 没有volatile 其他线程可能访问没有初始化的对象。

    // 1、 构造器私有化
    private DoubleCheckedLocking() {
    }

    // 3、 提供公共的静态方法
    public static DoubleCheckedLocking getInstance() {
        // 再次检测
        if (null != instance) { // 避免不必要的同步,已经存在对象
            return instance;
        }
        synchronized (DoubleCheckedLocking.class) {
            if (null == instance) {
                instance = new DoubleCheckedLocking();
                // 1. 开辟空间
                // 2. 初始化对象信息
                // 3. 返回对象地址给引用
            }
            return instance;
            // 如果 创建一个新的对象比较耗时,可能会发生指令重排的现象,这种情况下
            // 当 创建对象的过程还在继续时,就有可能直接返回instance,但此时,
            // instance 是空对象。所以我们必须使用volatile强制禁止代码重排
        }
    }

    // 通过观察两个对象的地址值是否相等,来查看是是否单例
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println(DoubleCheckedLocking.getInstance());
            
        });
        t.start();
        System.out.println(DoubleCheckedLocking.getInstance());
    }

}

​ 以下是结果:

/*
com.thread.other.DoubleCheckedLocking@6442b0a6
com.thread.other.DoubleCheckedLocking@6442b0a6
*/

6.5 ThreadLocal-每个线程本地存储区域

​ ThreadLocal即是每个线程本地存储区域,也就是每个线程的一亩三分地。

​ 我们可以把这个ThreadLocal看成一个银行。银行里面有很多保险箱,每个客户也就是每个线程都有自己的保险箱。它内部的存储结构类似于Map,key是线程的信息,值是对应的存储内容。ThreadLocal的好处就是,每个线程,它们都相互独立,却又能共享ThreadLocal这一个大的区域。这样我们就能使每个线程级别的数据存储,在多线程环境下达到成员变量的安全。因为每个数据只有线程自己能够看的到,别的线程看不到,不会影响其它线程。

  • 在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能够看见,不会影响其它线程。
  • ThreadLocal 能够放一个线程级别的变量,其本身能够被多个线程共享使用,并且又能够达到线程安全的目的。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全,常用的方法,就是get/set/initialValue 方法。
  • JDK建议ThreadLocal定义为private static,我们达到线程间共享
  • ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求、用户身份信息等,这时,一个线程的所有调用到的方法都可以非常方便地访问这些资源。
    • Hibernate的Session工具类HibernateUtil
    • 通过不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性。

尚学堂视频笔记六:多线程_第12张图片

​ 例子一,如何初始化线程本地变量:

/**
 * ThreadLocal : 每个线程自身的存储本地、局部区域
 * get/set/initialValue
 */

public class ThreadLocalTest {

    //    private static ThreadLocal threadLocal = new ThreadLocal<>();
    // 更改初始值
//    private static ThreadLocal threadLocal = new ThreadLocal<>(){
//        protected Integer initialValue(){
//            return 200;
//        }
//    };
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 200);

    public static void main(String[] args) {
        // 获取值
        System.out.println(Thread.currentThread().getName() + " --> " + threadLocal.get());

        // 设置值
        threadLocal.set(99);
        System.out.println(Thread.currentThread().getName() + " --> " + threadLocal.get());

        new Thread(new MyRun()).start();
        new Thread(new MyRun()).start();
        new Thread(new MyRun()).start();
        new Thread(new MyRun()).start();
    }
    public static class MyRun implements Runnable {

        @Override
        public void run() {
            threadLocal.set((int)(Math.random()*99));
            System.out.println(Thread.currentThread().getName() + " --> " + threadLocal.get());
        }
    }
}

​ 对于线程本地变量,使用泛型,ThreadLocal,可以存放相同类型的线程变量。它有集中初始化方法。

​ 第一种:

private static ThreadLocal threadLocal = new ThreadLocal<>()

​ 其中里面的线程的局部变量如果不设置默认为 null

​ 第二种,通过重写 ThreadLocal 中 initialValue 函数:

private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>() {
    protected Interger initialValue() {
        return 200;
    }
}

​ 第三种,使用lambda表达式:

private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 200);

​ 例子二:设置线程本地变量值,线程本地变量中的所有的线程都会初始化为该值:

public class ThreadLocalTest02 {

    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            new Thread(new MyRun()).start();
        }

    }

    public static class MyRun implements Runnable {

        @Override
        public void run() {
            Integer left = threadLocal.get();
            System.out.println(Thread.currentThread().getName() + "得到了 -- >" + left);
            threadLocal.set(left - 1);
            System.out.println(Thread.currentThread().getName() + "还剩下 -- >" + threadLocal.get());
        }
    }
}


​ 以下是结果:

/**
Thread-3得到了 -- >1
Thread-4得到了 -- >1
Thread-1得到了 -- >1
Thread-2得到了 -- >1
Thread-0得到了 -- >1
Thread-3还剩下 -- >0
Thread-1还剩下 -- >0
Thread-4还剩下 -- >0
Thread-0还剩下 -- >0
Thread-2还剩下 -- >0
**/

​ 例子三:每个线程都有自己的环境,其具体的初始值由开启它的线程决定:

/**
 * 分析上下文,也就是环境  起点
 * 1、构造器: 那里调用就属于哪里 找线程体
 * 2、run方法: 本线程自己的
 */

public class ThreadLocalTest03 {

    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

    public static void main(String[] args) {
        new Thread(new MyRun()).start();
    }

    public static class MyRun implements Runnable {

        public MyRun() {
            // 这里的方法还是main方法
            threadLocal.set(100);
            System.out.println(Thread.currentThread().getName() + " --> " + threadLocal.get());
        }

        @Override
        public void run() {
            // 这一块才是另外一个线程
            System.out.println(Thread.currentThread().getName() + " --> " + threadLocal.get());
        }
    }
}

​ 以下是结果:

/**
main --> 100
Thread-0 --> 1
**/

​ 例子四:InheritableThreadLocal-继承上下文环境的数据 拷贝一份给子线程:

/**
 * InheritableThreadLocal : 继承上下文环境的数据 拷贝一份给子线程
 *
 */

public class ThreadLocalTest04 {

    private static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set(2);
        System.out.println(Thread.currentThread().getName() + " -- > " + threadLocal.get());

        // 线程由main线程开辟
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " -- > " + threadLocal.get());
            threadLocal.set(200);
            System.out.println(Thread.currentThread().getName() + " -- > " + threadLocal.get());
        }).start();
    }

}

​ 以下是结果:

/**
main -- > 2
Thread-0 -- > 2
Thread-0 -- > 200
**/

6.6可重入锁原理的实现:

6.6.1 概念

​ 锁作为并发共享数据保证一致性的工具,大多数内置锁都是可重入的,也就是说,如果某个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立刻成功,并且会将这个锁的计数值加1。而当线程退出同步代码块时,计数器将会递减,当计数值等于0时,释放锁。如果没有可重入锁的支持,在第二次企图获得锁时将会进入死锁状态。可重入锁随处可见:

// 第一次获得锁
synchronized(this) {
	while(true) {
        // 第二次获得锁,应该是没有任何问题的,如果这里还要等的话,就会进入我们所说的		// 死锁,所以内置的锁都支持可重入。
        synchronized(this) {
            System.out.println("ReentrantLock");
        }
        try {
            Thread.sleep(1000);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ReentrantLockTest {
    public synchronized void a() {}
   	public synchronized void b() {}
    /**
    * 很明显的可重入锁用法
    */
    public synchronized void all() {
        this.a(); // 此时对象的锁计数值已经达到了2,为什么呢?因为一开始的all就已经					// +1了
        this.b();
    }
}

6.6.2 可重入锁的原理的实现:

​ 实际上就是进行判断,如果我们已经持有该对象的锁,那么我们继续执行,同时计数器加1;如果我们不持有该锁,那么我们我们进行锁定,并且计数器+1;当我们每退出一块代码,计数器就会-1,直到为0,释放该锁:

/**
 * 可重入的锁:锁不可以延续使用 + 计数器
 */

public class LockTest03 {

    ReLock lock = new ReLock();
    public void a() {
        try {
            lock.lock();
            System.out.println(lock.getHoldCount());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        doSomething();
        lock.unLock();
        System.out.println(lock.getHoldCount());
    }

    // 可重入
    public void doSomething() {
        try {
            lock.lock();
            System.out.println(lock.getHoldCount());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unLock();
    }

    public static void main(String[] args) {

        LockTest03 test = new LockTest03();
        test.a();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(test.lock.getHoldCount());
    }

}

// 可重入的锁
class ReLock {
    // 是否占用
    private boolean isLocked = false;
    private Thread lockedBy = null; // 存储线程
    // 所得计数器
    private int holdCount = 0;
    // 使用锁和释放锁
    public synchronized void lock() throws InterruptedException {
        while (isLocked && lockedBy != Thread.currentThread()) {
            wait();
        }

        isLocked = true;
        lockedBy = Thread.currentThread();
        holdCount ++;
    }
    // 释放锁
    public synchronized void unLock() {
        if(Thread.currentThread() == lockedBy) {
            holdCount --;
            if(holdCount == 0) {
                isLocked = false;
                notify();
                lockedBy = null;
            }
        }
    }

    public boolean isLocked() {
        return isLocked;
    }

    public Thread getLockedBy() {
        return lockedBy;
    }

    public int getHoldCount() {
        return holdCount;
    }

}

6.7 CAS-原子操作

6.7.1 锁的分类:

​ 锁有很多种分类:比如公平锁和不公平锁;悲观锁和乐观锁等等。这里看一下悲观锁和乐观锁。

  • 悲观锁:synchronized是独占锁也就是悲观锁,会导致其他所有需要锁的线程挂起,等待持有锁的线程释放资源。
  • 乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失就重试,直到成功为止。

6.7.2 Compare and Swap 比较并交换

  • 乐观锁的实现:
    • 有三个值:一个当前内存值V、旧的预期值A、将更新的值B。先获取到内存当中当前的内存值V,在将内存值V和原值A做比较,要是相等就修改为要修改的值B并返回true,否则什么都不做,并返回fasle;
    • CAS是一组原子操作,不会被外部打断;
    • 属于硬件级别的操作(利用CPU的CAS指令,通知借助JNI来完成的非阻塞算法),效率比加锁操作高。
    • ABA问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那么说明它的值没有被其他线程修改过了吗?如果在这段期间曾经被修改成B,然后又改回A,那CAS操作就会无任务它从来没有被修改过。

你可能感兴趣的:(Java基础,笔记,java,尚学堂,基础)