进程

  • 要想说线程,首先必须得聊聊进程,因为线程是依赖于进程存在的。

进程概述

  • 进程是程序的一次执行过程,是系统运行程序的基本单位,打开 Windows 的任务管理器就可以看到很多进程。
  • 概念:进程就是正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。
  • 多进程:每个进程都拥有自己独立的资源,多个进程可在单核处理器上并发执行,在多核处理器上并行执行。

多进程的意义

  • 单进程计算机只能做一件事情。而我们现在的计算机都可以一边玩游戏(游戏进程),一边听音乐(音乐进程),所以我们常见的操作系统都是多进程操作系统。比如:Windows,Mac和Linux等,能在同一个时间段内执行多个任务。
  • 对于单核计算机来讲,游戏进程和音乐进程是同时运行的吗?不是。
  • 因为CPU在某个时间点上只能做一件事情,计算机是在游戏进程和音乐进程间做着频繁切换,且切换速度很快,
  • 所以,我们感觉游戏和音乐在同时进行,其实并不是同时执行的。多进程的作用不是提高执行速度,而是提高CPU的使用率。

进程状态有哪些

  • 进程状态有哪些
    • 运行状态:进程正在处理机上运行。在单处理机环境下,每一时刻最多只有一个进程处于运行状态。
    • 就绪状态:进程已处于准备运行的状态,即进程获得了除处理机之外的一切所需资源,一旦得到处理机即可运行。
    • 阻塞状态,又称等待状态:进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理机)或等待输入/输出完成。即使处理机空闲,该进程也不能运行。

进程状态转换

  • 注意区别就绪状态和等待状态:
    • 就绪状态是指进程仅缺少处理机,只要获得处理机资源就立即执行;而等待状态是指进程需要其他资源(除了处理机)或等待某一事件。
  • 就绪状态 -> 运行状态:
    • 处于就绪状态的进程被调度后,获得处理机资源(分派处理机时间片),于是进程由就绪状态转换为运行状态。
  • 运行状态 -> 就绪状态:
    • 处于运行状态的进程在时间片用完后,不得不让出处理机,从而进程由运行状态转换为就绪状态。此外,在可剥夺的操作系统中,当有更高优先级的进程就绪时,调度程度将正执行的进程转换为就绪状态,让更高优先级的进程执行。
  • 运行状态 -> 阻塞状态:
    • 当进程请求某一资源(如外设)的使用和分配或等待某一事件的发生(如I/O操作的完成)时,它就从运行状态转换为阻塞状态。进程以系统调用的形式请求操作系统提供服务,这是一种特殊的、由运行用户态程序调用操作系统内核过程的形式。
  • 阻塞状态 -> 就绪状态:
    • 当进程等待的事件到来时,如I/O操作结束或中断结束时,中断处理程序必须把相应进程的状态由阻塞状态转换为就绪状态。

进程通信

  • 进程通信是指进程之间的信息交换。PV操作是低级通信方式,髙级通信方式是指以较高的效率传输大量数据的通信方式。高级通信方法主要有以下三个类。
  • 共享存储
    • 在通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行写/读操作实现进程之间的信息交换。在对共享空间进行写/读操作时,需要使用同步互斥工具(如 P操作、V操作),对共享空间的写/读进行控制。共享存储又分为两种:低级方式的共享是基于数据结构的共享;高级方式则是基于存储区的共享。操作系统只负责为通信进程提供可共享使用的存储空间和同步互斥工具,而数据交换则由用户自己安排读/写指令完成。
    • 需要注意的是,用户进程空间一般都是独立的,要想让两个用户进程共享空间必须通过特殊的系统调用实现,而进程内的线程是自然共享进程空间的。
  • 消息传递
    • 在消息传递系统中,进程间的数据交换是以格式化的消息(Message)为单位的。若通信的进程之间不存在可直接访问的共享空间,则必须利用操作系统提供的消息传递方法实现进程通信。进程通过系统提供的发送消息和接收消息两个原语进行数据交换。
      1. 直接通信方式:发送进程直接把消息发送给接收进程,并将它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息。
      1. 间接通信方式:发送进程把消息发送到某个中间实体中,接收进程从中间实体中取得消息。这种中间实体一般称为信箱,这种通信方式又称为信箱通信方式。该通信方式广泛应用于计算机网络中,相应的通信系统称为电子邮件系统。
  • 管道通信
    • 管道通信是消息传递的一种特殊方式。所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入(写)管道;而接收管道输出的接收进程(即读进程),则从管道中接收(读)数据。为了协调双方的通信,管道机制必须提供以下三方面的协调能力:互斥、同步和确定对方的存在。

进程同步

  • 多进程虽然提高了系统资源利用率和吞吐量,但是由于进程的异步性可能造成系统的混乱。进程同步的任务就是对多个相关进程在执行顺序上进行协调,使并发执行的多个进程之间可以有效的共享资源和相互合作,保证程序执行的可再现性
  • 同步机制需要遵循的原则:
    • 1.空闲让进:当没有进程处于临界区的时候,应该许可其他进程进入临界区的申请
    • 2.忙则等待:当前如果有进程处于临界区,如果有其他进程申请进入,则必须等待,保证对临界区的互斥访问
    • 3.有限等待:对要求访问临界资源的进程,需要在有限时间内进入临界区,防止出现死等
    • 4.让权等待:当进程无法进入临界区的时候,需要释放处理机,边陷入忙等
  • 经典的进程同步问题:生产者-消费者问题;哲学家进餐问题;读者-写者问题
  • 同步的解决方案:管程,信号量。

用户态和核心态

  • 当程序运行在3级特权级上时,就可以称之为运行在用户态,因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;反之,当程序运行在级特权级上时,就可以称之为运行在内核态。
  • 虽然用户态下和内核态下工作的程序有很多差别,但最重要的差别就在于特权级的不同,即权力的不同。 运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。
  • 当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态。
  • 用户态切换到内核态的3种方式
      1. 系统调用:这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
      1. 异常:当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
      1. 外围设备的中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

进程死锁

  • 死锁是指多个进程在运行过程中,因为争夺资源而造成的一种僵局,如果没有外力推进,处于僵局中的进程就无法继续执行。
  • 死锁原因:
    • 1.竞争资源:请求同一有限资源的进程数多于可用资源数
    • 2.进程推进顺序非法:进程执行中,请求和释放资源顺序不合理,如资源等待链
  • 死锁产生的必要条件:
    • 1.互斥条件:进程对所分配的资源进行排他性的使用
    • 2.请求和保持条件:进程被阻塞的时候并不释放锁申请到的资源
    • 3.不可剥夺条件:进程对于已经申请到的资源在使用完成之前不可以被剥夺
    • 4.环路等待条件:发生死锁的时候存在的一个 进程-资源 环形等待链
  • 死锁处理:
    • 预防死锁:破坏产生死锁的4个必要条件中的一个或者多个;实现起来比较简单,但是如果限制过于严格会降低系统资源利用率以及吞吐量
    • 避免死锁:在资源的动态分配中,防止系统进入不安全状态(可能产生死锁的状态)-如银行家算法
    • 检测死锁:允许系统运行过程中产生死锁,在死锁发生之后,采用一定的算法进行检测,并确定与死锁相关的资源和进程,采取相关方法清除检测到的死锁。实现难度大
    • 解除死锁:与死锁检测配合,将系统从死锁中解脱出来(撤销进程或者剥夺资源)。对检测到的和死锁相关的进程以及资源,通过撤销或者挂起的方式,释放一些资源并将其分配给处于阻塞状态的进程,使其转变为就绪态。实现难度大

进程调度算法

  • 先来先服务调度算法FCFS: 既可以作为作业调度算法也可以作为进程调度算法;按作业或者进程到达的先后顺序依次调度;因此对于长作业比较有利;
  • 短作业优先调度算法SJF: 作业调度算法,算法从就绪队列中选择估计时间最短的作业进行处理,直到得出结果或者无法继续执行;缺点:不利于长作业;未考虑作业的重要性;运行时间是预估的,并不靠谱 ;
  • 高响应比算法HRN: 响应比=(等待时间+要求服务时间)/要求服务时间;
  • 时间片轮转调度RR: 按到达的先后对进程放入队列中,然后给队首进程分配CPU时间片,时间片用完之后计时器发出中断,暂停当前进程并将其放到队列尾部,循环 ;
  • 多级反馈队列调度算法: 目前公认较好的调度算法;设置多个就绪队列并为每个队列设置不同的优先级,第一个队列优先级最高,其余依次递减。优先级越高的队列分配的时间片越短,进程到达之后按FCFS放入第一个队列,如果调度执行后没有完成,那么放到第二个队列尾部等待调度,如果第二次调度仍然没有完成,放入第三队列尾部……只有当前一个队列为空的时候才会去调度下一个队列的进程。

线程

  • 在一个进程内部又可以执行多个任务,而这每一个任务我们就可以看成是一个线程。是程序使用CPU的基本单位。
  • 线程与进程相似,但线程是一个比进程更小的执行单位,一个进程在其执行的过程中可能产生多个线程。
  • 多线程:一个进程可由多个线程组成,多个线程共享进程内资源,多个线程可在单核处理器上并发执行,在多核处理器并行执行。

多线程有什么意义

  • 多线程的作用不是提高执行速度,而是为了提高应用程序的使用率。
  • 那么怎么理解这个问题呢?我们程序在运行的使用,都是在抢CPU的时间片(执行权),如果是多线程的程序,那么在抢到CPU的执行权的概率应该比较单线程程序抢到的概率要大.那么也就是说,CPU在多线程程序中执行的时间要比单线程多,所以就提高了程序的使用率.但是即使是多线程程序,那么他们中的哪个线程能抢占到CPU的资源呢,这个是不确定的,所以多线程具有随机性.

并行和并发

  • 并行是逻辑上同时发生,指在某一个时间内同时运行多个程序。在同一时刻多个任务同时执行,或者说是在同一时刻可以执行多条程序指令,多核处理器才可以做到。
    • 在多核处理器上,并发和并行同时存在,处理器上的每个核同一时刻同时执行多个任务,每个核在很短的时间段内又同时执行多个任务,对多任务粗略划分是多个进程,对进程划分可能又是多个线程。
    • 同一时刻,处理器的每个核只能运行一个进程中的一个线程中的一条指令(Intel 的超线程技术,如双核四线程,四核八线程,处理器的线程(硬件上)和进程中的线程(软件上)不是一个概念,这个所谓的超线程技术也并不能达到真正的多核效果,只是提高了处理器的吞吐量核利用率)。
  • 并发是物理上同时发生,指在某一个时间点同时运行多个程序。在一段时间内多个任务同时执行,或者说是在一段很短的时间内可以执行多条程序指令,微观上看起来好像是可以同时运行多个进程,单核处理器就可以做到。

Java程序运行原理

  • Java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。
  • 该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。

JVM的启动是多线程的吗

  • JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。

进程与线程区别

概念区别

  • 进程: 进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位(具有动态、并发、独立、异步的特性,以及就绪、执行、阻塞3种状态);引入进程是为了使多个程序可以并发的执行,以提高系统的资源利用率和吞吐量。
  • 线程: 是比进程更小的可独立运行的基本单位,可以看做是轻量级的进程(具有轻型实体,独立调度分派单位,可并发执行,共享进程资源等属性);引入目的是为了减少程序在并发执行过程中的开销,使OS的并发效率更高。

调度区别

  • 调度方面:在引入线程的OS中,线程是独立的调度和分派单位,而进程作为资源的拥有单位(相当于把未引入线程的传统OS中的进程的两个属性分开了)。
  • 由于线程不拥有资源,因此可以显著的提高并发度以及减少切换开销。

并发行区别

  • 并发性:引入了线程的OS中,进程间可以并发,而且一个进程内部的多个线程之间也是可以并发的,这就使OS具有更好的并发性,有效的提高了系统资源利用率和吞吐量。

拥有资源对比

  • 拥有资源:无论OS是否支持线程,进程都是基本的资源拥有单位,线程只拥有很少的基本的资源,但是线程可以访问所隶属的进程的资源(进程的代码段,数据段和所拥有的系统资源如fd)

系统开销对比

  • 系统开销:创建或者撤销进程的时候,系统要为之创建或回收PCB,系统资源等,切换时也需要保存和恢复CPU环境。
  • 而线程的切换只需要保存和恢复少量的寄存器,不涉及存储器管理方面的工作,所以开销较小。此外,统一进程中的多个线程由于共享地址空间,所以通信同步等都比较方便。

线程创建

继承Thread类

  • 创建步骤如下

    • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
    • 创建Thread子类的实例,即创建了线程对象。
    • 调用线程对象的start()方法来启动该线程。
  • 代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    /**
    第一种方式的步骤:
    1: 定义一个类,让该类去继承Thread类
    2: 重写run方法
    3: 创建该类的对象
    4: 启动线程
    */

    public class ThreadDemo {
    public static void main(String[] args) {
    // 创建对象
    MyThread t1 = new MyThread() ;
    MyThread t2 = new MyThread() ;
    // 启动线程: 需要使用start方法启动线程, 如果我们在这里调用的是run方法,那么我们只是把该方法作为普通方法进行执行
    // t1.run() ;
    // t1.run() ;
    t1.start() ; // 告诉jvm开启一个线程调用run方法
    // t1.start() ; // 一个线程只能被启动一次
    t2.start() ;

    }
    }

    public class MyThread extends Thread {
    @Override
    public void run() {
    for(int x = 0 ; x < 1000 ; x++) {
    System.out.println(x);
    }
    }
    }

通过Runnable接口

  • 创建步骤如下

    • 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    • 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
    • 调用线程对象的start()方法来启动该线程。
  • 代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /**
    实现多线程的第二中方式步骤:
    1: 定义一个类,让该类去实现Runnable接口
    2: 重写run方法
    3: 创建定义的类的对象
    4: 创建Thread的对象吧第三步创建的对象作为参数传递进来
    5: 启动线程
    */
    public static void main(String[] args) {
    // 创建定义的类的对象
    MyThread mt = new MyThread() ;
    // 创建Thread的对象吧第三步创建的对象作为参数传递进来
    Thread t1 = new Thread(mt , "张三") ;
    Thread t2 = new Thread(mt , "李四") ;
    // 启动线程
    t1.start() ;
    t2.start() ;
    }

    public class MyThread implements Runnable {
    @Override
    public void run() {
    for(int x = 0 ; x < 1000 ; x++) {
    System.out.println(Thread.currentThread().getName() + "---" + x);
    }

    }
    }

通过Callable和Future

  • Callable基础介绍

    • Runnable 从 JDK1.0 开始就有了,Callable 是在 JDK1.5 增加的。它们的主要区别是 Callable 的 call() 方法可以返回值和抛出异常,而 Runnable 的 run() 方法没有这些功能。Callable 可以返回装载有计算结果的 Future 对象。

    • 通过对比两个接口得到这样的结论

      • Callable 接口下的方法是 call(),Runnable 接口的方法是 run();
      • Callable 的任务执行后可返回值,而 Runnable 的任务是不能返回值的;
      • call() 方法可以抛出异常,run()方法不可以的;
      • 运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future 对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果;
      1
      2
      3
      4
      5
      6
      7
      public interface Runnable {
      public void run();
      }

      public interface Callable<V> {
      V call() throws Exception;
      }
  • 创建步骤如下所示

    • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
    • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
    • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
    • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值,调用get()方法会阻塞线程。
  • 代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class CallableThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
    int i = 0;
    for (; i < 100; i++) {
    System.out.println(Thread.currentThread().getName() + " " + i);
    }
    return i;
    }
    }


    public class ThreadDemo {
    public static void main(String[] args) {
    CallableThread ctt = new CallableThread();
    FutureTask<Integer> ft = new FutureTask<>(ctt);
    for (int i = 0; i < 100; i++) {
    System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
    if (i == 20) {
    new Thread(ft, "有返回值的线程").start();
    }
    }
    try {
    System.out.println("子线程的返回值:" + ft.get());
    } catch (InterruptedException e) {
    e.printStackTrace();
    } catch (ExecutionException e) {
    e.printStackTrace();
    }
    }
    }

三种创建线程区别

  • 采用实现Runnable、Callable接口的方式创见多线程时

    • 优势是:
      • 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
      • 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
    • 劣势是:
      • 编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
  • 使用继承Thread类的方式创建多线程时

    • 优势是:
      • 编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
    • 劣势是:
      • 线程类已经继承了Thread类,所以不能再继承其他父类。
  • Runnable和Callable区别

    • Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
    • 这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。

run方法的作用

  • 为什么要重写run方法
    • 可以在定义的类中,定义多个方法,而方法中的代码并不是所有的都需要线程来进行执行;如果我们想让某一个段代码被线程,那么我们只需要将那一段代码放在run方法中。那么也就是说run方法中封装的都是要被线程执行的代码 ;
  • run方法中的代码的特点:
    • 封装的都是一些比较耗时的代码

start和run区别

  • 线程中start和run方法有什么区别?
    • 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?这是一个非常经典的java多线程面试问题。
    • 当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码。

为何不能重复start

  • 如下所示,可以发现即使多次调用start方法,线程只会被执行一次。那么这个究竟是怎么做到的呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class ThreadDemo {
    public static void main(String[] args) {
    // 创建对象
    MyThread t1 = new MyThread() ;
    // 启动线程: 需要使用start方法启动线程, 如果我们在这里调用的是run方法,那么我们只是把该方法作为普通方法进行执行
    // t1.run() ;
    t1.start() ; // 告诉jvm开启一个线程调用run方法
    // t1.start() ; // 一个线程只能被启动一次
    }
    }

    public class MyThread extends Thread {
    @Override
    public void run() {
    System.out.println("小杨逗比");
    }
    }


    Exception in thread "main" java.lang.IllegalThreadStateException
    at java.lang.Thread.start(Thread.java:708)
    at top.fulsun.TestThread02.main(TestThread02.java:10) // 第二次调用start()的位置报错
    小杨逗比
  • 然后查看一下start方法的源码,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    boolean started = false;
    public synchronized void start() {
    // Android-changed: throw if 'started' is true
    if (threadStatus != 0 || started)
    throw new IllegalThreadStateException();
    group.add(this);
    started = false;
    try {
    nativeCreate(this, stackSize, daemon);
    started = true;
    } finally {
    try {
    if (!started) {
    group.threadStartFailed(this);
    }
    } catch (Throwable ignore) {
    /* do nothing. If start0 threw a Throwable then
    it will be passed up the call stack */
    }
    }
    }
  • 一个线程多次start会出现什么情况?

    • 会直接抛出异常 IllegalThreadStateException

线程的生命周期

咱们先来瞅瞅源码定义的状态(为了突出重点,我把注释都去掉了):

1
2
3
4
5
6
7
8
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}

能够清楚的看到,在源码中能够清楚的看到,在源码中定义了 6 种线程状态。

  • NEW 到 RUNNABLE ,应该是挺容易理解的,就是 thread 调用了 start 方法

    • Java 刚创建出来的 Thread 对象就是 NEW 状态,创建 Thread 对象主要有两种方法,一种是继承 Thread 对象,重写 run() 方法,一种是实现 Runnable 接口,重写 run() 方法,并将该实现类作为创建 Thread 对象的参数
    • NEW 只是说,这个线程在编程语言层面创建了,在操作系统层面还没有创建,那当然就不会被操作系统调度了对不对,就更谈不上执行了
    • 所以 Java 线程如果想要执行的话,就必须转换到 RUNNABLE 状态,也就是 thread 调用 start 方法
  • RUNNABLE 与 BLOCKED ,如果线程等待 synchronized 的隐式锁时,就会从 RUNNABLE 状态转到 BLOCKED 状态。因为 synchronized 修饰的方法/代码块同一时刻只允许一个线程执行,所以其他线程就只能等待了呗,当等待的线程获得 synchronized 隐式锁时,就会从 BLOCKED 状态转到 RUNNABLE 状态

  • 在这里有没有个疑问?就是线程在 wait 一个条件发生时,在操作系统层面线程会转到 waiting 状态,那么在 JVM 层面呢?在 JVM 层面, Java 线程状态是不会发生变化的。也就是这个时候 Java 线程的状态依然是 RUNNABLE 状态

  • RUNNABLE 与 WAITING 状态转换

    • 有三种情况会触发 RUNNABLE 和 WAITING 之间的转换:

      • 场景一:在 synchronized 代码块中调用 object.wait() 方法
      • 场景二:处于 RUNNABLE 状态的线程调用thread.join()方法等待某个线程运行完成。例如thread1中有一行代码是thread2.join()则执行这行代码后thread1会从RUNNABLE 状态转换成 WAITING 状态,直到thread2执行完成以后,thread1才会从 WAITING 状态再次回到 RUNNABLE 状态。
      • 场景三:调用LockSupport.park()方法,会让当前线程从RUNNABLE 转换为 WAITING。当某个线程调用了LockSupport.unpark(thread)时,thread方法就会从WAITING状态转换成RUNNABLE状态。
  • RUNNABLE 与 TIMED_WAITING 状态转换

    • 仔细观察下会发现, TIMED_WAITING 与 WAITING 相比,就是多了超时参数,毕竟 TIMED_WAITING 是有时限等待嘛

    • 有四种场景可以使得线程从 RUNNABLE 状态转换到 TIMED_WAITING 状态:

      • 场景一:Object.wait(long timeout)
      • 场景二:Thread.join(long timeout)
      • 场景三:LockSupport.parkUntil(long deadline) (还有一个park型方法,这里不列举了)
      • 场景四:Thread.sleep(long timeout)

      可以看出,这四种场景的前三种都是上面提到的函数的带时间参数的形式,最后一个是我们最直接可以想到的sleep。

  • RUNNABLE 到 TERMINATED ,这个过程比较好理解,线程执行完 run() 方法之后,就自动到 TERMINATED 状态了,当然了如果在执行 run() 方法过程中有异常抛出,也会导致线程终止

    • 强制中断 run() 方法的执行,怎么办呢?是使用 stop() 方法还是 interrupt() 方法呢?正确的姿势是调用 interrupt() 方法
    • stop() 方法会真的杀死线程,不给线程一点儿喘息的机会,如果被杀死的线程持有 synchronized 隐式锁,那就再也不会释放掉这个锁了,接下来的线程也就没办法获得 synchronized 隐式锁,是不是特别危险?同样 suspend() 和 resume() 这两个方法也是不建议使用
    • interrupt() 方法相比于 stop() 方法就温柔很多,它只是通知线程后续的操作可以不用去执行了,线程可以选择执行现在就不执行,当然也可以选择再执行一段时间后再停止,或者我就不听你的,非要执行完,都没关系, interrupt() 只是通知一下你而已。就比如你要做火车去一个地方,突然通知你这个火车晚点了,你可以选择无视这个通知继续等待,或者选择另外一趟高铁,但是不管你做什么,和火车站都没啥关系,它通知的责任尽到了

wait和sleep方法

  • wait和sleep方法的区别
    • 最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。
  • wait()和sleep()其他区别
    • sleep来自Thread类,和wait来自Object类
    • 调用sleep()方法的过程中,线程不会释放对象锁。而 调用 wait 方法线程会释放对象锁
    • sleep睡眠后不出让系统资源,wait让出系统资源其他线程可以占用CPU
    • sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒。
  • 通俗解释
    • Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而 sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。

线程调度

线程的调度问题

  • 应用程序在执行的时候都需要依赖于线程去抢占CPU的时间片 , 谁抢占到了CPU的时间片,那么CPU就会执行谁
  • 线程的执行:假如我们的计算机只有一个 CPU,那么 CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。

线程有两种调度模型

  • 分时调度模型
    • 所有线程轮流使用CPU的使用权,平均分配每个线程占用 CPU 的时间片
  • 抢占式调度模型
    • 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。Java使用的是抢占式调度模型。
    • java 中的优先级1-10,1最低, 10 最高。main主线程,优先级为5。优先级高,cpu调用的次数多。并不是绝对,优先级需要看系统等

线程控制

休眠线程

  • sleep方法
    • public static void sleep(long time) ;
    • time表达的意思是休眠的时间 , 单位是毫秒
  • sleep方法具体作用
    • 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
    • 让其他线程有机会继续执行,但它并不释放对象锁。也就是如果有Synchronized同步块,其他线程仍然不能访问共享数据。注意该方法要捕获异常。
  • sleep如何使低优先级线程执行
    • 比如有两个线程同时执行(没有Synchronized),一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完成后,低优先级的线程才能执行;但当高优先级的线程sleep(5000)后,低优先级就有机会执行了。
    • 总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。
  • 怎么唤醒一个阻塞的线程?
    • 如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
  • Thread.sleep(0)的作用是啥?
    • 由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

wait和sleep方法

  • wait和sleep方法的区别
    • 最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。
  • wait()和sleep()其他区别
    • sleep来自Thread类,和wait来自Object类
    • 调用sleep()方法的过程中,线程不会释放对象锁。而 调用 wait 方法线程会释放对象锁
    • sleep睡眠后不出让系统资源,wait让出系统资源其他线程可以占用CPU
    • sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒。
  • 通俗解释
    • Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而 sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。

加入线程

  • join方法

    • public final void join()
    • 等待该线程执行完毕了以后,其他线程才能再次执行。Thread的非静态方法join()让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前,B不能工作。
    • 保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有存活,则当前线程不需要停止。
  • join()有什么作用?

    • Thread的join()的含义是等待该线程终止,即将挂起调用线程的执行,直到被调用的对象完成它的执行。比如存在两个线程t1和t2,下述代码表示先启动t1,直到t1的任务结束,才轮到t2启动。
    1
    2
    3
    t1.start();
    t1.join();
    t2.start();
  • 注意事项:

    • 在线程启动之后,才能调用调用方法。如果没有启动,调用该方法,则直接会……
  • join与start调用顺序问题

    • join方法必须在线程start方法调用之后调用才有意义。这个也很容易理解:如果一个线程都没有start,那它也就无法同步了。因为执行完start方法才会创建线程。

join方法实现原理

  • join方法是通过调用线程的wait方法来达到同步的目的的。例如A线程中调用了B线程的join方法,则相当于在A线程中调用了B线程的wait方法,当B线程执行完(或者到达等待时间),B线程会自动调用自身的notifyAll方法唤醒A线程,从而达到同步的目的。

  • join方法源码

    • 如下所示,由下面的join方法源码可以看到:

      • 如果join方法传参为0的话,则会调用isAlive()方法,一直检测线程是否存活(执行完毕),如果存活就调用wait方法,一直阻塞。isAlive()判断线程是否还活着,即线程是否还未终止。
      • 如果参数为负数,则直接报错:”timeout value is negative”
      • 如果参数大于0,则while里面一直判断线程是否存活,存活的话就一直判断当前线程执行的时间并与计算还需要等待多久时间,最后如果等待时间小于等于0就跳出循环,否则就继续wait
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      public final synchronized void join(long millis) throws InterruptedException {
      long base = System.currentTimeMillis();
      long now = 0;

      if (millis < 0) {
      throw new IllegalArgumentException("timeout value is negative");
      }

      if (millis == 0) {
      while (isAlive()) {
      wait(0);
      }
      } else {
      while (isAlive()) {
      long delay = millis - now;
      if (delay <= 0) {
      break;
      }
      wait(delay);
      now = System.currentTimeMillis() - base;
      }
      }
      }

礼让线程

  • yield方法
    • public static void yield():
    • 暂停当前正在执行的线程对象,并执行其他线程。
  • 线程礼让的原理是:
    • 暂停当前的线程,然CPU去执行其他的线程,这个暂定的时间是相当短暂的;当我某一个线程暂定完毕以后,其他的线程还没有抢占到cpu的执行权 ;那么这个是时候当前的线程会和其他的线程再次抢占cpu的执行权;
  • yield礼让线程会释放锁吗
    • yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。

守护线程

  • setDaemon方法

    • public final void setDaemon(boolean on),默认false.
    • 将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
  • 注意要点:

    • 该方法必须在启动线程前调用。
    • jvm会线程程序中存在的线程类型,如果线程全部是守护线程,那么jvm就停止。
  • 当主线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。如:Java垃圾回收线程就是一个典型的守护线程;内存资源或者线程的管理,但是非守护线程也可以。它的存在,必定有它的意义,只需在乎我们怎么把它用到恰到好处。

中断线程

  • stop方法(不推荐)
    • public final void stop():
    • 停止线程的运行
  • interrupt方法
    • public void interrupt():
    • 中断线程(这个翻译不太好),查看API可得当线程调用wait(),sleep(long time)方法的时候处于阻塞状态,可以通过这个方法清除阻塞

启动线程

  • start() 它的作用是启动一个新线程。

    • 通过start()方法来启动的新线程,处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行相应线程的run()方法。

    • run方法运行结束,此线程随即终止。start()不能被重复调用。用start方法来启动线程,真正实现了多线程运行,即无需等待某个线程的run方法体代码执行完毕就直接继续执行下面的代码。这里无需等待run方法执行完毕,即可继续执行下面的代码,即进行了线程切换。

    • run()就和普通的成员方法一样,可以被重复调用。
      如果直接调用run方法,并不会启动新线程!程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。

线程关闭

  • 线程对象属于一次性消耗品,一般线程执行完run方法之后,线程就正常结束了,线程结束之后就报废了,不能再次start,只能新建一个线程对象。但有时run方法是永远不会结束的。例如在程序中使用线程进行Socket监听请求,或是其他的需要循环处理的任务。在这种情况下,一般是将这些任务放在一个循环中,如while循环。当需要结束线程时,如何退出线程呢?

  • 结束Thread线程的几种方法

    • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
    • 使用interrupt()方法中断线程
    • 使用stop方法强行终止线程(不推荐使用,可能发生不可预料的结果)
    • 前两种方法都可以实现线程的正常退出,也就是要谈的优雅结束线程;第3种方法相当于电脑断电关机一样,是不安全的方法。

使用退出标志终止线程

  • 使用一个变量来控制循环,例如最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。代码如下:

  • 定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class ThreadSafe extends Thread {
    public volatile boolean exit = false;
    public void run() {
    while (!exit){
    //do something
    }
    }
    }

    /**
    * stop thread running
    */
    public void stop() {
    if (exit ) {
    exit = false;
    }
    }

使用interrupt()方法终止线程

  • 使用interrupt()方法来终端线程可分为两种情况:

  • 线程处于阻塞状态,如使用了sleep,同步锁的wait,socket的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,系统会抛出一个InterruptedException异常,代码中通过捕获异常,然后break跳出循环状态,使线程正常结束。通常很多人认为只要调用interrupt方法线程就会结束,实际上是错的,一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正常结束run方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class ThreadSafe extends Thread {
    public void run() {
    while (true){
    try{
    Thread.sleep(5*1000);阻塞5
    }catch(InterruptedException e){
    e.printStackTrace();
    break;//捕获到异常之后,执行break跳出循环。
    }
    }
    }
    }
  • 线程未进入阻塞状态,使用isInterrupted()判断线程的中断标志来退出循环,当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。

    1
    2
    3
    4
    5
    6
    7
    public class ThreadSafe extends Thread {
    public void run() {
    while (!isInterrupted()){
    //do something, but no throw InterruptedException
    }
    }
    }
  • 为什么要区分进入阻塞状态和和非阻塞状态两种情况了,是因为当阻塞状态时,如果有interrupt()发生,系统除了会抛出InterruptedException异常外,还会调用interrupted()函数,调用时能获取到中断状态是true的状态,调用完之后会复位中断状态为false,所以异常抛出之后通过isInterrupted()是获取不到中断状态是true的状态,从而不能退出循环,因此在线程未进入阻塞的代码段时是可以通过isInterrupted()来判断中断是否发生来控制循环,在进入阻塞状态后要通过捕获异常来退出循环。因此使用interrupt()来退出线程的最好的方式应该是两种情况都要考虑:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class ThreadSafe extends Thread {
    public void run() {
    while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
    try{
    Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
    }catch(InterruptedException e){
    e.printStackTrace();
    break;//捕获到异常之后,执行break跳出循环。
    }
    }
    }
    }

使用stop方法终止线程

  • 程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果
  • 不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。

线程间通信

wait()/notify()

  • Object类中相关的方法有notify方法和wait方法。因为wait和notify方法定义在Object类中,因此会被所有的类所继承。这些方法都是final的,即它们都是不能被重写的,不能通过子类覆写去改变它们的行为。
    • ①wait()方法: 让当前线程进入等待,并释放锁。
    • ②wait(long)方法: 让当前线程进入等待,并释放锁,不过等待时间为long,超过这个时间没有对当前线程进行唤醒,将自动唤醒
    • ③notify()方法: 让当前线程通知那些处于等待状态的线程,当前线程执行完毕后释放锁,并从其他线程中唤醒其中一个继续执行。
    • ④notifyAll()方法: 让当前线程通知那些处于等待状态的线程,当前线程执行完毕后释放锁,将唤醒所有等待状态的线程。

wait()方法使用注意事项

  • ①当前的线程必须拥有当前对象的monitor,也即lock,就是锁,才能调用wait()方法,否则将抛出异常java.lang.IllegalMonitorStateException。
  • ②线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行。
  • ③要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中。
  • wait()与sleep()比较
    • 当线程调用了wait()方法时,它会释放掉对象的锁。
    • Thread.sleep(),它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的。

notify()方法使用注意事项

  • ①如果多个线程在等待,它们中的一个将会选择被唤醒。这种选择是随意的,和具体实现有关。(线程等待一个对象的锁是由于调用了wait()方法)。
  • ②被唤醒的线程是不能被执行的,需要等到当前线程放弃这个对象的锁,当前线程会在方法执行完毕后释放锁。

wait()/notify()协作

  • 如果通知过早,则会打乱程序的运行逻辑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public static void main(String[] args) throws InterruptedException {
    MyRun run = new MyRun();
    Thread bThread = new Thread(run.runnableB);
    bThread.start();
    Thread.sleep(100);
    Thread aThread = new Thread(run.runnableA);
    aThread.start();
    }

    public class MyRun {
    private String lock = new String("");
    public Runnable runnableA = new Runnable() {

    @Override
    public void run() {
    try {
    synchronized (lock) {
    System.out.println("begin wait");
    lock.wait();
    System.out.println("end wait");
    }
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    }
    };
    public Runnable runnableB = new Runnable() {
    @Override
    public void run() {
    synchronized (lock) {
    System.out.println("begin notify");
    // 如果notify方法先执行,将导致wait方法释放锁进入等待状态后
    // 永远无法被唤醒,影响程序逻辑。应避免这种情况。
    lock.notify();
    System.out.println("end notify");
    }
    }
    };
    }
  • 等待wait的条件发生变化

    • 也就是wait等待条件发生了变化,也容易造成程序逻辑的混乱。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    //Add类,执行加法操作,然后通知Subtract类
    public class Add {
    private String lock;

    public Add(String lock) {
    super();
    this.lock = lock;
    }
    public void add(){
    synchronized (lock) {
    ValueObject.list.add("anyThing");
    lock.notifyAll();
    }
    }
    }
    //Subtract类,执行减法操作,执行完后进入等待状态,等待Add类唤醒notify
    public class Subtract {
    private String lock;

    public Subtract(String lock) {
    super();
    this.lock = lock;
    }
    public void subtract(){
    try {
    synchronized (lock) {
    if(ValueObject.list.size()==0){
    System.out.println("wait begin ThreadName="+Thread.currentThread().getName());
    lock.wait();
    System.out.println("wait end ThreadName="+Thread.currentThread().getName());
    }
    ValueObject.list.remove(0);
    System.out.println("list size ="+ValueObject.list.size());
    }
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

    //线程ThreadAdd
    public class ThreadAdd extends Thread{
    private Add pAdd;

    public ThreadAdd(Add pAdd) {
    super();
    this.pAdd = pAdd;
    }
    @Override
    public void run() {
    pAdd.add();
    }

    }


    // 线程ThreadSubtract
    public class ThreadSubtract extends Thread{
    private Subtract rSubtract;

    public ThreadSubtract(Subtract rSubtract) {
    super();
    this.rSubtract = rSubtract;
    }
    @Override
    public void run() {
    rSubtract.subtract();
    }
    }
    • 启动线程

      • 先开启两个ThreadSubtract线程,由于list中没有元素,进入等待状态。
      • 再开启一个ThreadAdd线程,向list中增加一个元素,然后唤醒两个ThreadSubtract线程。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public static void main(String[] args) throws InterruptedException {
      String lock = new String("");
      Add add = new Add(lock);
      Subtract subtract = new Subtract(lock);
      ThreadSubtract subtractThread1 = new ThreadSubtract(subtract);
      subtractThread1.setName("subtractThread1");
      subtractThread1.start();
      ThreadSubtract subtractThread2 = new ThreadSubtract(subtract);
      subtractThread2.setName("subtractThread2");
      subtractThread2.start();
      Thread.sleep(1000);
      ThreadAdd addThread = new ThreadAdd(add);
      addThread.setName("addThread");
      addThread.start();
      }
    • 输出结果

      • 当第二个ThreadSubtract线程执行减法操作时,抛出下标越界异常。
      • 一开始两个ThreadSubtract线程等待状态,当ThreadAdd线程添加一个元素并唤醒所有线程后,
        • 第一个ThreadSubtract线程接着原来的执行到的地点开始继续执行,删除一个元素并输出集合大小。
        • 同样,第二个ThreadSubtract线程也如此,可是此时集合中已经没有元素了,所以抛出异常。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      > wait begin ThreadName=subtractThread1
      > wait begin ThreadName=subtractThread2
      > wait end ThreadName=subtractThread2
      > Exception in thread "subtractThread1" list size =0
      > wait end ThreadName=subtractThread1
      > java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
      > at java.util.ArrayList.rangeCheck\(Unknown Source\)
      > at java.util.ArrayList.remove\(Unknown Source\)
      > at com.lvr.communication.Subtract.subtract\(Subtract.java:18\)
      > at com.lvr.communication.ThreadSubtract.run\(ThreadSubtract.java:12\)
    • 解决办法:从等待状态被唤醒后,重新判断条件,看看是否扔需要进入等待状态,不需要进入再进行下一步操作。即把if()判断,改成while()。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public void subtract(){
      try {
      synchronized (lock) {
      while(ValueObject.list.size()==0){
      System.out.println("wait begin ThreadName="+Thread.currentThread().getName());
      lock.wait();
      System.out.println("wait end ThreadName="+Thread.currentThread().getName());
      }
      ValueObject.list.remove(0);
      System.out.println("list size ="+ValueObject.list.size());
      }
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      }

Condition实现等待/通知

Condition简单介绍

  • 关键字synchronized与wait()和notify()/notifyAll()方法相结合可以实现等待/通知模式,类似ReentrantLock也可以实现同样的功能,但需要借助于Condition对象。
  • 关于Condition实现等待/通知就不详细介绍了,可以完全类比wait()/notify(),基本使用和注意事项完全一致。
  • 就只简单介绍下类比情况:
    • condition.await()————>lock.wait()
    • condition.await(long time, TimeUnit unit)————>lock.wait(long timeout)
    • condition.signal()————>lock.notify()
    • condition.signaAll()————>lock.notifyAll()

Condition实现方式

  • 特殊之处:synchronized相当于整个ReentrantLock对象只有一个单一的Condition对象情况。而一个ReentrantLock却可以拥有多个Condition对象,来实现通知部分线程。
  • 具体实现方式:
    • 假设有两个Condition对象:ConditionA和ConditionB。那么由ConditionA.await()方法进入等待状态的线程,由ConditionA.signalAll()通知唤醒;由ConditionB.await()方法进入等待状态的线程,由ConditionB.signalAll()通知唤醒。

生产者消费者模型

  • 生产者消费者模型发生场景

    • 多线程-并发协作(生产者消费者模型)。多线程同步的经典问题!
  • 什么是生产者消费者模型,准确说应该是“生产者-消费者-仓储”模型举例式说明

    • 生产者仅仅在仓储未满时候生产,仓满则停止生产。
    • 消费者仅仅在仓储有产品时候才能消费,仓空则等待。
    • 当消费者发现仓储没产品可消费时候会通知生产者生产。
    • 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。
  • 专业术语说明什么是生产者消费者模型

    • 生产者消费者模型通过一个缓存队列,既解决了生产者和消费者之间强耦合的问题,又平衡了生产者和消费者的处理能力。
    • 具体规则:生产者只在缓存区未满时进行生产,缓存区满时生产者进程被阻塞;消费者只在缓存区非空时进行消费,缓存区为空时消费者进程被阻塞;当消费者发现缓存区为空时会通知生产者生产;当生产者发现缓存区满时会通知消费者消费。
    • 实现关键:synchronized保证对象只能被一个线程占用;wait()让当前线程进入等待状态,并释放它所持有的锁;notify()&notifyAll()唤醒一个(所有)正处于等待状态的线程

一生产与一消费案例

  • 下面代码案例是一个生产者,一个消费者的模式。

    • 假设场景:一个String对象,其中生产者为其设置值,消费者拿走其中的值,不断的循环往复,实现生产者/消费者的情形。
    • 实现方式:wait()/notify()实现
  • 生产者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class Product {
    private String lock;

    public Product(String lock) {
    super();
    this.lock = lock;
    }
    public void setValue(){
    try {
    synchronized (lock) {
    if(!StringObject.value.equals("")){
    //有值,不生产
    lock.wait();
    }
    String value = System.currentTimeMillis()+""+System.nanoTime();
    System.out.println("set的值是:"+value);
    StringObject.value = value;
    lock.notify();
    }
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
  • 消费者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class Consumer {
    private String lock;

    public Consumer(String lock) {
    super();
    this.lock = lock;
    }
    public void getValue(){
    try {
    synchronized (lock) {
    if(StringObject.value.equals("")){
    //没值,不进行消费
    lock.wait();
    }
    System.out.println("get的值是:"+StringObject.value);
    StringObject.value = "";
    lock.notify();
    }
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
  • 生产者线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class ThreadProduct extends Thread{
    private Product product;

    public ThreadProduct(Product product) {
    super();
    this.product = product;
    }
    @Override
    public void run() {
    //死循环,不断的生产
    while(true){
    product.setValue();
    }
    }
    }
  • 消费者线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class ThreadConsumer extends Thread{
    private Consumer consumer;

    public ThreadConsumer(Consumer consumer) {
    super();
    this.consumer = consumer;
    }
    @Override
    public void run() {
    //死循环,不断的消费
    while(true){
    consumer.getValue();
    }
    }
    }
  • 开启生产者/消费者模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Test {
    public static void main(String[] args) throws InterruptedException {
    String lock = new String("");
    Product product = new Product(lock);
    Consumer consumer = new Consumer(lock);
    ThreadProduct pThread = new ThreadProduct(product);
    ThreadConsumer cThread = new ThreadConsumer(consumer);
    pThread.start();
    cThread.start();
    }
    }
  • 输出结果:

    1
    2
    3
    4
    5
    6
    set的值是:148827033184127168687409691
    get的值是:148827033184127168687409691
    set的值是:148827033184127168687449887
    get的值是:148827033184127168687449887
    set的值是:148827033184127168687475117
    get的值是:148827033184127168687475117

多生产与多消费案例

  • 特殊情况: 按照上述一生产与一消费的情况,通过创建多个生产者和消费者线程,实现多生产与多消费的情况,将会出现“假死”。

  • 具体原因: 多个生产者和消费者线程。当全部运行后,生产者线程生产数据后,可能唤醒的同类即生产者线程。此时可能会出现如下情况:所有生产者线程进入等待状态,然后消费者线程消费完数据后,再次唤醒的还是消费者线程,直至所有消费者线程都进入等待状态,此时将进入“假死”。

  • 解决方法: 将notify()或signal()方法改为notifyAll()或signalAll()方法,这样就不怕因为唤醒同类而进入“假死”状态了。

  • Condition方式实现

  • 生产者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class Product {
    private ReentrantLock lock;
    private Condition condition;

    public Product(ReentrantLock lock, Condition condition) {
    super();
    this.lock = lock;
    this.condition = condition;
    }

    public void setValue() {
    try {
    lock.lock();
    while (!StringObject.value.equals("")) {
    // 有值,不生产
    condition.await();
    }
    String value = System.currentTimeMillis() + "" + System.nanoTime();
    System.out.println("set的值是:" + value);
    StringObject.value = value;
    condition.signalAll();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }finally {
    lock.unlock();
    }
    }
    }
  • 消费者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class Consumer {
    private ReentrantLock lock;
    private Condition condition;

    public Consumer(ReentrantLock lock,Condition condition) {
    super();
    this.lock = lock;
    this.condition = condition;
    }
    public void getValue(){
    try {
    lock.lock();
    while(StringObject.value.equals("")){
    //没值,不进行消费
    condition.await();
    }
    System.out.println("get的值是:"+StringObject.value);
    StringObject.value = "";
    condition.signalAll();

    } catch (InterruptedException e) {
    e.printStackTrace();
    }finally {
    lock.unlock();
    }
    }
    }
    • 生产者线程和消费者线程与一生产一消费的模式相同。
  • 开启多生产/多消费模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static void main(String[] args) throws InterruptedException {
    ReentrantLock lock = new ReentrantLock();
    Condition newCondition = lock.newCondition();
    Product product = new Product(lock,newCondition);
    Consumer consumer = new Consumer(lock,newCondition);
    for(int i=0;i<3;i++){
    ThreadProduct pThread = new ThreadProduct(product);
    ThreadConsumer cThread = new ThreadConsumer(consumer);
    pThread.start();
    cThread.start();
    }
    }
  • 输出结果:

    1
    2
    3
    4
    set的值是:148827212374628960540784817
    get的值是:148827212374628960540784817
    set的值是:148827212374628960540810047
    get的值是:148827212374628960540810047
  • 可见交替地进行get/set实现多生产/多消费模式。

    • 注意:相比一生产一消费的模式,改动了两处。
    • ①signal()–>signalAll()避免进入“假死”状态。
    • ②if()判断–>while()循环,重新判断条件,避免逻辑混乱。

会遇到哪些关键问题

  • 如何保证同一资源被多个线程并发访问时的完整性。常用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。
  • 如何保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区空时消耗数据?
  • 实际开发时案例
    • 这种并发情况下,一般服务端程序用的比较多,Android端的应用程序较少有什么并发情况。虽然事实如此,但是构建生产者-消费者模型,是线程间协作的思想,工作线程的协助是为了让UI线程更好的完成工作,提高用户体验。比如,图片选择查看器案例!

如何解决关键问题

  • 如何保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区空时消耗数据,思考一下?
  • 解决思路可以简单概括为:
    • 生产者持续生产,直到缓冲区满,满时阻塞;缓冲区不满后,继续生产;
    • 消费者持续消费,直到缓冲区空,空时阻塞;缓冲区不空后,继续消费;
    • 生产者和消费者都可以有多个;
  • 能够让消费者和生产者在各自满足条件需要阻塞时能够起到正确的作用
    • wait()/notify()方式;
    • await()/signal()方式;
    • BlockingQueue阻塞队列方式;
    • PipedInputStream/PipedOutputStream方式;
  • 一般可以使用第一种和第三种方式实现逻辑。

多线程并发

继承Thread类的方式卖电影票案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ThreadDemo {
public static void main(String[] args) {
/**
* 需求:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
*/
// 创建3个线程对象
SellTicktes t1 = new SellTicktes() ;
SellTicktes t2 = new SellTicktes() ;
SellTicktes t3 = new SellTicktes() ;
// 设置名称
t1.setName("窗口1") ;
t2.setName("窗口2") ;
t3.setName("窗口3") ;
// 启动线程
t1.start() ;
t2.start() ;
t3.start() ;
}
}

public class SellTicktes extends Thread {
private static int num = 100 ;
@Override
public void run() {
/**
* 定义总票数
*
* 如果我们把票数定义成了局部变量,那么表示的意思是每一个窗口出售了各自的100张票; 而我们的需求是: 总共有100张票
* 而这100张票要被3个窗口出售; 因此我们就不能把票数定义成局部变量,只能定义成成员变量
*/
// 模拟售票
while(true) {
if( num > 0 ) {
System.out.println(Thread.currentThread().getName() + "正在出售" + (num--) + "张票");
}
}
}
}
  • 结果如下

    1
    2
    3
    4
    5
    窗口1正在出售100张票
    窗口2正在出售99张票
    窗口3正在出售100张票
    窗口2正在出售97张票
    ...

实现Runnable接口的方式卖电影票

  • 代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class SellTicektesDemo {
    public static void main(String[] args) {
    // 创建SellTicektes对象
    SellTicektes st = new SellTicektes() ;
    // 创建Thread对象
    Thread t1 = new Thread(st , "窗口1") ;
    Thread t2 = new Thread(st , "窗口2") ;
    Thread t3 = new Thread(st , "窗口3") ;
    // 启动线程
    t1.start() ;
    t2.start() ;
    t3.start() ;
    }
    }

    public class SellTicektes implements Runnable {
    private static int num = 100 ;
    @Override
    public void run() {
    while(true) {
    if(num > 0) {
    System.out.println(Thread.currentThread().getName() + "正在出售第" + (num--) + "张票");
    }
    }
    }
    }
  • 运行结果

    1
    2
    3
    4
    5
    ....
    窗口1正在出售 4 张票
    窗口2正在出售 1 张票
    窗口1正在出售 0 张票
    窗口3正在出售 -1 张票

同票和负数票的原因分析

  • 讲解过电影院售票程序,从表面上看不出什么问题,但是在真实生活中,售票时网络是不能实时传输的,总是存在延迟的情况,所以,在出售一张票以后,需要一点时间的延迟。改实现接口方式的卖票程序,每次卖票延迟100毫秒

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class ThreadDemo {
    public static void main(String[] args) {
    // 创建3个线程对象
    SellTicktes t1 = new SellTicktes() ;
    SellTicktes t2 = new SellTicktes() ;
    SellTicktes t3 = new SellTicktes() ;
    // 设置名称
    t1.setName("窗口1") ;
    t2.setName("窗口2") ;
    t3.setName("窗口3") ;
    // 启动线程
    t1.start() ;
    t2.start() ;
    t3.start() ;
    }
    }

    public class SellTicktes extends Thread {
    private static int num = 100 ;
    @Override
    public void run() {
    // 模拟售票
    while(true) {
    if( num > 0 ) {
    try {
    Thread.sleep(100) ;
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "正在出售" + (num--) + "张票");
    }
    }
    }
    }
  • 第一步和第二步之间加入了延时操作,那么一个线程就可能在还没有对票数进行减操作之前,其他线程就已经将票数减少了,这样就会出现票数为负的情况。

线程安全问题的产生原因分析

  • 首先想为什么出现问题?
  • 是否是多线程环境,是否有共享数据,是否有多条语句操作共享数据
  • 如何解决多线程安全问题呢?
    • 基本思想:让程序没有安全问题的环境。怎么实现呢?把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。

同步代码块的方式解决线程安全问题

  • 同步代码块的格式

    • 同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能
    1
    2
    3
    synchronized(对象){
    需要同步的代码;
    }
  • 同步代码块优势和劣势

    • 同步的好处:同步的出现解决了多线程的安全问题。
    • 同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
  • 代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    aspackage top.fulsun.tick;

    public class ThreadDemo03 {
    public static void main(String[] args) {
    // 创建3个线程对象
    SellTicktes3 t1 = new SellTicktes3();
    Thread t2 = new Thread(t1);
    Thread t3 = new Thread(t1);
    // 设置名称
    t1.setName("窗口1");
    t2.setName("窗口2");
    t3.setName("窗口3");
    // 启动线程
    t1.start();
    t2.start();
    t3.start();
    }
    }

    class SellTicktes3 extends Thread {
    private static int num = 20;

    @Override
    public void run() {
    // 模拟售票
    while (true) {
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    synchronized (this) {
    if(num > 0) {
    System.out.println(Thread.currentThread().getName() + "正在出售" + (num--) + "张票");
    }
    }
    }
    }
    }
  • 如果是不同的对象创建的线程,不能使用this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    package top.fulsun.tick;

    public class ThreadDemo03 {
    public static void main(String[] args) {
    // 创建3个线程对象
    SellTicktes3 t1 = new SellTicktes3();
    // Thread t2 = new Thread(t1);
    // Thread t3 = new Thread(t1);
    SellTicktes3 t2 = new SellTicktes3();
    SellTicktes3 t3 = new SellTicktes3();
    // 设置名称
    t1.setName("窗口1");
    t2.setName("窗口2");
    t3.setName("窗口3");
    // 启动线程
    t1.start();
    t2.start();
    t3.start();
    }
    }

    class SellTicktes3 extends Thread {
    private static int num = 20;
    private static Object obj = new Object();

    @Override
    public void run() {
    // 模拟售票
    while (true) {
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    synchronized (obj) {
    // synchronized (this) {
    if(num > 0) {
    System.out.println(Thread.currentThread().getName() + "正在出售" + (num--) + "张票");
    }
    }
    }
    }


    }

线程安全特性

什么是线程安全

  • 什么是线程安全
    • 线程安全就是当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
  • 并发切入点是什么?
    • 并发分析的切入点分为两个核心,三大性质。两大核心:JMM内存模型(主内存和工作内存)以及happens-before;三条性质:原子性,可见性,有序性。

线程安全级别

  • 线程安全也是有几个级别
    • 不可变:
      • 像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
    • 绝对线程安全
      • 不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
    • 相对线程安全
      • 相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
    • 线程非安全
      • ArrayList、LinkedList、HashMap等都是线程非安全的类.

多线程三要素

  • 三要素分别是:原子性,可见性,有序性

如何理解原子性

  • 如何理解原子性
    • 即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。
  • 举一个例子
    • 关于原子性,一个非常经典的例子就是银行转账问题:比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。

如何理解可见性

  • 如何理解可见性
    • 当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。
  • 举一个例子
    • CPU从主内存中读数据的效率相对来说不高,现在主流的计算机中,都有几级缓存。每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。这一点是操作系统或者说是硬件层面的机制,所以很多应用开发人员经常会忽略。

如何理解有序性

  • 如何理解有序性
    • 顺序性指的是,程序执行的顺序按照代码的先后顺序执行。
  • 举一个例子
    • 语句应该依次执行,但实际上JVM真正在执行这段代码时,并不保证它们一定完全按照此顺序执行。处理器为了提高程序整体的执行效率,JVM可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。CPU不按照我的代码顺序执行代码,那怎么保证得到我们想要的效果呢?实际上,CPU虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。

三要素作用

  • 上面这三个要素主要作用是保障线程安全。保证线程安全可从多线程三特性出发:
    • 原子性(Atomicity):单个或多个操作是要么全部执行,要么都不执行
      • Lock:保证同时只有一个线程能拿到锁,并执行申请锁和释放锁的代码
      • synchronized:对线程加独占锁,被它修饰的类/方法/变量只允许一个线程访问
    • 可见性(Visibility):当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
      • volatile:保证新值能立即同步到主内存,且每次使用前立即从主内存刷新;
      • synchronized:在释放锁之前会将工作内存新值更新到主存中
    • 有序性(Ordering):程序代码按照指令顺序执行
      • volatile: 本身就包含了禁止指令重排序的语义
      • synchronized:保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入

处理多线程并发

保证原子性

第一种方式:锁和同步

  • 常用的保证Java操作原子性的工具是锁和同步方法(或者同步代码块)。

  • 使用锁

    • 代码形式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public void testLock () {
      Lock lock = new ReentrantLock();
      lock.lock();
      try {
      // access the resource protected by this lock
      } finally {
      lock.unlock();
      }
      }
    • 可以保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个线程能执行申请锁和释放锁之间的代码。

  • 同步方法或者同步代码块。

    • 使用非静态同步方法时,锁住的是当前实例;使用静态同步方法时,锁住的是该类的Class对象;使用静态代码块时,锁住的是synchronized关键字后面括号内的对象。下面是同步代码块示例

      1
      2
      3
      4
      5
      6
      public void testLock () {
      synchronized (MainActivity.class){
      int j = i;
      i = j + 1;
      }
      }
  • 总结

    • 无论使用锁还是synchronized,本质都是一样,通过锁来实现资源的排它性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段的原子性。这是一种以牺牲性能为代价的方法。

第二种方式:CAS

  • 基础类型变量自增(i++)是一种常被误以为是原子操作而实际不是的操作。

    • Java中提供了对应的原子操作类来实现该操作,并保证原子性,其本质是利用了CPU级别的CAS指令。由于是CPU级别的指令,其开销比需要操作系统参与的锁的开销小。AtomicInteger使用方法如下。
    1
    2
    3
    4
    5
    6
    7
    8
    AtomicInteger atomicInteger = new AtomicInteger();
    for(int b = 0; b < numThreads; b++) {
    new Thread(() -> {
    for(int a = 0; a < iteration; a++) {
    atomicInteger.incrementAndGet();
    }
    }).start();
    }

保证可见性

  • Java提供了volatile关键字来保证可见性。

    • 当使用volatile修饰某个变量时,它会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值。

    • 双检锁/双重校验锁创建单例模式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      public class Singleton {
      //创建 SingleObject 的一个对象
      private static Singleton instance;

      //让构造函数为 private,这样该类就不会被实例化
      private Singleton (){}

      //获取唯一可用的对象
      public static SingletionObject getInstance() {
      if(instance != null) { // 第一次检查:为避免在实例已经创建的情况下每次获取实例都加锁取(影响性能)
      return instance;
      }
      //synchronized关键字保证的可见性是防止多个线程实例化出多个instance。
      synchronized (SingletionTest.class) {
      if(instance != null) { //第二次检查:锁内不再次判断,会导致实例重复创建
      return instance;
      }
      // 隐患:同步块中修改的变量在释放锁之前对其他线程“可见”
      // (1)分配内存空间。
      // (2)初始化对象
      // (3)将内存空间的地址赋值给对应的引用。
      // 如果先执行了3,但是没有初始化
      // 这个时候新线程(可见)第一次检查不通过,直接返回对象,会返回了一个尚未初始化完成的对象
      // 如果将instance设置volatile类型变量,
      //在volatile写操作之前的任何操作都是不可重排序的,即23的顺序不可重排序。
      instance = new Singleton();
      return instance;
      }
      }
      }

保证有序性

  • Java中可通过volatile在一定程序上保证顺序性,另外还可以通过synchronized和锁来保证顺序性。
  • synchronized和锁保证顺序性的原理和保证原子性一样,都是通过保证同一时间只会有一个线程执行目标代码段来实现的。
  • 除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为happens-before原则隐式地保证顺序性。两个操作的执行顺序只要可以通过happens-before推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不作任何保证,可对其进行任意必要的重新排序以获取高效率。

线程池

实际开发问题

  • 在我们的开发中经常会使用到多线程。例如在Android中,由于主线程的诸多限制,像网络请求等一些耗时的操作我们必须在子线程中运行。
  • 我们往往会通过new Thread来开启一个子线程,待子线程操作完成以后通过Handler切换到主线程中运行。这么以来我们无法管理我们所创建的子线程,并且无限制的创建子线程,它们相互之间竞争,很有可能由于占用过多资源而导致死机或者OOM。所以在Java中为我们提供了线程池来管理我们所创建的线程。

线程池的优势

  • ①降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  • ②提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
  • ③方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
  • ④更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。

ThreadPoolExecutor

  • 可以通过ThreadPoolExecutor来创建一个线程池。

    1
    ExecutorService service = new ThreadPoolExecutor(....);
  • 下面我们就来看一下ThreadPoolExecutor中的一个构造方法。

    1
    2
    3
    4
    5
    6
    7
    public ThreadPoolExecutor(int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)
  • ThreadPoolExecutor参数含义

  • 1.corePoolSize

    • 线程池中的核心线程数,默认情况下,核心线程一直存活在线程池中,即便他们在线程池中处于闲置状态。除非我们将ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这时候处于闲置的核心线程在等待新任务到来时会有超时策略,这个超时时间由keepAliveTime来指定。一旦超过所设置的超时时间,闲置的核心线程就会被终止。
  • 2.maximumPoolSize

    • 线程池中所容纳的最大线程数,如果活动的线程达到这个数值以后,后续的新任务将会被阻塞。包含核心线程数+非核心线程数。
  • 3.keepAliveTime

    • 非核心线程闲置时的超时时长,对于非核心线程,闲置时间超过这个时间,非核心线程就会被回收。只有对ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这个超时时间才会对核心线程产生效果。
  • 4.unit

    • 用于指定keepAliveTime参数的时间单位。他是一个枚举,可以使用的单位有天(TimeUnit.DAYS),小时(TimeUnit.HOURS),分钟(TimeUnit.MINUTES),毫秒(TimeUnit.MILLISECONDS),微秒(TimeUnit.MICROSECONDS, 千分之一毫秒)和毫微秒(TimeUnit.NANOSECONDS, 千分之一微秒);
  • 5.workQueue

    • 线程池中保存等待执行的任务的阻塞队列。通过线程池中的execute方法提交的Runable对象都会存储在该队列中。我们可以选择下面几个阻塞队列。我们还能够通过实现BlockingQueue接口来自定义我们所需要的阻塞队列。

      阻塞队列 说明
      ArrayBlockingQueue 基于数组实现的有界的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。
      LinkedBlockingQueue 基于链表实现的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。
      SynchronousQueue 内部没有任何容量的阻塞队列。在它内部没有任何的缓存空间。对于SynchronousQueue中的数据元素只有当我们试着取走的时候才可能存在。
      PriorityBlockingQueue 具有优先级的无限阻塞队列。
  • 6.threadFactory

    • 线程工厂,为线程池提供新线程的创建。ThreadFactory是一个接口,里面只有一个newThread方法。 默认为DefaultThreadFactory类。
  • 7.handler

    • 是RejectedExecutionHandler对象,而RejectedExecutionHandler是一个接口,里面只有一个rejectedExecution方法。当任务队列已满并且线程池中的活动线程已经达到所限定的最大值或者是无法成功执行任务,这时候ThreadPoolExecutor会调用RejectedExecutionHandler中的rejectedExecution方法。在ThreadPoolExecutor中有四个内部类实现了RejectedExecutionHandler接口。在线程池中它默认是AbortPolicy,在无法处理新任务时抛出RejectedExecutionException异常

    • 下面是在ThreadPoolExecutor中提供的四个可选值。

    • 我们也可以通过实现RejectedExecutionHandler接口来自定义我们自己的handler。如记录日志或持久化不能处理的任务。

      可选值 说明
      CallerRunsPolicy 只用调用者所在线程来运行任务。
      AbortPolicy 直接抛出RejectedExecutionException异常。
      DiscardPolicy 丢弃掉该任务,不进行处理。
      DiscardOldestPolicy 丢弃队列里最近的一个任务,并执行当前任务。

ThreadPoolExecutor使用

  • 如下所示

    1
    ExecutorService service = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
  • 对于ThreadPoolExecutor有多个构造方法,对于上面的构造方法中的其他参数都采用默认值。可以通过execute和submit两种方式来向线程池提交一个任务。

  • execute

    • 当我们使用execute来提交任务时,由于execute方法没有返回值,所以说我们也就无法判定任务是否被线程池执行成功。
    1
    2
    3
    4
    5
    service.execute(new Runnable() {
    public void run() {
    System.out.println("execute方式");
    }
    });
  • submit

    • 当我们使用submit来提交任务时,它会返回一个future,我们就可以通过这个future来判断任务是否执行成功,还可以通过future的get方法来获取返回值。如果子线程任务没有完成,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时候有可能任务并没有执行完。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Future<Integer> future = service.submit(new Callable<Integer>() {

    @Override
    public Integer call() throws Exception {
    System.out.println("submit方式");
    return 2;
    }
    });
    try {
    Integer number = future.get();
    } catch (ExecutionException e) {
    e.printStackTrace();
    }
  • 线程池关闭

    • 调用线程池的shutdown()shutdownNow()方法来关闭线程池
    • shutdown原理:将线程池状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
    • shutdownNow原理:将线程池的状态设置成STOP状态,然后中断所有任务(包括正在执行的)的线程,并返回等待执行任务的列表。
    • 中断采用interrupt方法,所以无法响应中断的任务可能永远无法终止。 但调用上述的两个关闭之一,isShutdown()方法返回值为true,当所有任务都已关闭,表示线程池关闭完成,则isTerminated()方法返回值为true。当需要立刻中断所有的线程,不一定需要执行完任务,可直接调用shutdownNow()方法。

线程池执行流程

  • 大概的流程图如下
  • 文字描述如下
    • ①如果在线程池中的线程数量没有达到核心的线程数量,这时候就回启动一个核心线程来执行任务。
    • ②如果线程池中的线程数量已经超过核心线程数,这时候任务就会被插入到任务队列中排队等待执行。
    • ③由于任务队列已满,无法将任务插入到任务队列中。这个时候如果线程池中的线程数量没有达到线程池所设定的最大值,那么这时候就会立即启动一个非核心线程来执行任务。
    • ④如果线程池中的数量达到了所规定的最大值,那么就会拒绝执行此任务,这时候就会调用RejectedExecutionHandler中的rejectedExecution方法来通知调用者。

四种线程池类

  • Java中四种具有不同功能常见的线程池。
    • 他们都是直接或者间接配置ThreadPoolExecutor来实现他们各自的功能。这四种线程池分别是newFixedThreadPool,newCachedThreadPool,newScheduledThreadPool和newSingleThreadExecutor。这四个线程池可以通过Executors类获取。

newFixedThreadPool

  • 通过Executors中的newFixedThreadPool方法来创建,该线程池是一种线程数量固定的线程池。

    1
    ExecutorService service = Executors.newFixedThreadPool(4);
  • 在这个线程池中 所容纳最大的线程数就是我们设置的核心线程数。

    • 如果线程池的线程处于空闲状态的话,它们并不会被回收,除非是这个线程池被关闭。如果所有的线程都处于活动状态的话,新任务就会处于等待状态,直到有线程空闲出来。
    • 由于newFixedThreadPool只有核心线程,并且这些线程都不会被回收,也就是它能够更快速的响应外界请求
  • 从下面的newFixedThreadPool方法的实现可以看出,newFixedThreadPool只有核心线程,并且不存在超时机制,采用LinkedBlockingQueue,所以对于任务队列的大小也是没有限制的。

    1
    2
    3
    4
    5
    public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>());
    }

newCachedThreadPool

  • 通过Executors中的newCachedThreadPool方法来创建。

    1
    2
    3
    4
    5
    public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>());
    }
  • 通过s上面的newCachedThreadPool方法在这里我们可以看出它的 核心线程数为0, 线程池的最大线程数Integer.MAX_VALUE。而Integer.MAX_VALUE是一个很大的数,也差不多可以说 这个线程池中的最大线程数可以任意大。

    • 当线程池中的线程都处于活动状态的时候,线程池就会创建一个新的线程来处理任务。该线程池中的线程超时时长为60秒,所以当线程处于闲置状态超过60秒的时候便会被回收。
    • 这也就意味着若是整个线程池的线程都处于闲置状态超过60秒以后,在newCachedThreadPool线程池中是不存在任何线程的,所以这时候它几乎不占用任何的系统资源。
    • 对于newCachedThreadPool他的任务队列采用的是SynchronousQueue,上面说到在SynchronousQueue内部没有任何容量的阻塞队列。SynchronousQueue内部相当于一个空集合,我们无法将一个任务插入到SynchronousQueue中。所以说在线程池中如果现有线程无法接收任务,将会创建新的线程来执行任务。

newScheduledThreadPool

  • 通过Executors中的newScheduledThreadPool方法来创建。

    1
    2
    3
    4
    5
    6
    7
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
    new DelayedWorkQueue());
    }
  • 它的核心线程数是固定的,对于非核心线程几乎可以说是没有限制的,并且当非核心线程处于限制状态的时候就会立即被回收。

    • 创建一个可定时执行或周期执行任务的线程池:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
    service.schedule(new Runnable() {
    public void run() {
    System.out.println(Thread.currentThread().getName()+"延迟三秒执行");
    }
    }, 3, TimeUnit.SECONDS);
    service.scheduleAtFixedRate(new Runnable() {
    public void run() {
    System.out.println(Thread.currentThread().getName()+"延迟三秒后每隔2秒执行");
    }
    }, 3, 2, TimeUnit.SECONDS);
    • 输出结果:

      1
      2
      3
      4
      5
      pool-1-thread-2延迟三秒后每隔2秒执行
      pool-1-thread-1延迟三秒执行
      pool-1-thread-1延迟三秒后每隔2秒执行
      pool-1-thread-2延迟三秒后每隔2秒执行
      pool-1-thread-2延迟三秒后每隔2秒执行
  • 部分方法说明

    • schedule(Runnable command, long delay, TimeUnit unit):延迟一定时间后执行Runnable任务;
    • schedule(Callable callable, long delay, TimeUnit unit):延迟一定时间后执行Callable任务;
    • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):延迟一定时间后,以间隔period时间的频率周期性地执行任务;
    • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit):与scheduleAtFixedRate()方法很类似,但是不同的是scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔,而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,也就是这一些任务系列的触发时间都是可预知的。
  • ScheduledExecutorService功能强大,对于定时执行的任务,建议多采用该方法。

newSingleThreadExecutor

  • 通过Executors中的newSingleThreadExecutor方法来创建,在这个线程池中只有一个核心线程,对于任务队列没有大小限制,也就意味着这一个任务处于活动状态时,其他任务都会在任务队列中排队等候依次执行

  • newSingleThreadExecutor将所有的外界任务统一到一个线程中支持,所以在这个任务执行之间我们不需要处理线程同步的问题。

    1
    2
    3
    4
    5
    6
    public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
    (new ThreadPoolExecutor(1, 1,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>()));
    }

线程池的使用技巧

  • 需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。(N代表CPU个数)

    任务类别 说明
    CPU密集型任务 线程池中线程个数应尽量少,如配置N+1个线程的线程池。
    IO密集型任务 由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率,如2*N。
    混合型任务 可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。

面试问题

并发问题

  • 平时项目中使用锁和synchronized比较多,而很少使用volatile,难道就没有保证可见性?
  • 锁可以保证可见性?
    • 锁和synchronized即可以保证原子性,也可以保证可见性。都是通过保证同一时间只有一个线程执行目标代码段来实现的。
  • 锁和synchronized即可保证原子性也可保证可见性,为何还需要volatile?
    • synchronized和锁需要通过操作系统来仲裁谁获得锁,开销比较高,而volatile开销小很多。因此在只需要保证可见性的条件下,使用volatile的性能要比使用锁和synchronized高得多。
  • 既然锁和synchronized可以保证原子性,为什么还需要AtomicInteger这种的类来保证原子操作?
    • 锁和synchronized需要通过操作系统来仲裁谁获得锁,开销比较高,而AtomicInteger是通过CPU级的CAS操作来保证原子性,开销比较小。所以使用AtomicInteger的目的还是为了提高性能。
  • synchronized即可修饰非静态方式,也可修饰静态方法,还可修饰代码块,有何区别
    • synchronized修饰非静态同步方法时,锁住的是当前实例;synchronized修饰静态同步方法时,锁住的是该类的Class对象;synchronized修饰静态代码块时,锁住的是synchronized关键字后面括号内的对象。

线程问题

  • 经典面试题:现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

    • Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    /**
    * 现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
    */
    private void test2(){
    Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
    Log.d("线程执行","Thread1");
    }
    });
    Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
    Log.d("线程执行","Thread2");
    }
    });
    Thread t3 = new Thread(new Runnable() {
    @Override
    public void run() {
    Log.d("线程执行","Thread3");
    }
    });

    t1.start();
    try {
    t1.join();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    t2.start();
    try {
    t2.join();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    t3.start();
    try {
    t3.join();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }


    //执行结果
    2019-05-23 18:12:16.633 14354-14423/cn.ycbjie.ycthreadpool D/线程执行: Thread1
    2019-05-23 18:12:16.634 14354-14424/cn.ycbjie.ycthreadpool D/线程执行: Thread2
    2019-05-23 18:12:16.635 14354-14425/cn.ycbjie.ycthreadpool D/线程执行: Thread3
  • 另一种写法,发现并不是按照t1、t2、t3的执行顺序的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    /**
    * 现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
    */
    private void test2(){
    Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
    Log.d("线程执行","Thread1");
    }
    });
    Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
    Log.d("线程执行","Thread2");
    }
    });
    Thread t3 = new Thread(new Runnable() {
    @Override
    public void run() {
    Log.d("线程执行","Thread3");
    }
    });

    t1.start();
    t2.start();
    t3.start();


    try {
    t1.join();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    try {
    t2.join();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    try {
    t3.join();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }

    //执行结果
    2019-05-23 18:21:58.182 14868-14939/cn.ycbjie.ycthreadpool D/线程执行: Thread3
    2019-05-23 18:21:58.182 14868-14937/cn.ycbjie.ycthreadpool D/线程执行: Thread1
    2019-05-23 18:21:58.182 14868-14938/cn.ycbjie.ycthreadpool D/线程执行: Thread2
  • 如果不适用join,还可以用什么方式,使用锁也可以实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    private void test3(){
    final ShareThread sh = new ShareThread();
    new Thread(new Runnable() {

    @Override
    public void run() {
    try {
    sh.Test01();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }, "T1").start();
    new Thread(new Runnable() {

    @Override
    public void run() {
    try {
    sh.Test02();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }, "T2").start();
    new Thread(new Runnable() {

    @Override
    public void run() {
    try {
    sh.Test03();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }, "T3").start();
    }


    class ShareThread {

    // flag作为标记
    private int flag = 1;
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    public void Test01() throws InterruptedException {
    lock.lock();
    try {
    while (flag != 1) {
    c1.await();
    }
    System.out.println("正在执行的是:" + Thread.currentThread().getName());
    flag = 2;
    c2.signal();// 通知一个线程来执行
    } finally {
    lock.unlock();
    }
    }

    public void Test02() throws InterruptedException {
    lock.lock();
    try {
    while (flag != 2) {
    c2.await();
    }
    System.out.println("正在执行的是:" + Thread.currentThread().getName());
    flag = 3;
    c3.signal();// 通知一个线程来执行
    } finally {
    lock.unlock();
    }
    }

    public void Test03() throws InterruptedException {
    lock.lock();
    try {
    while (flag != 3) {
    c3.await();
    }
    System.out.println("正在执行的是:" + Thread.currentThread().getName());
    flag = 1;
    c1.signal();// 通知一个线程来执行
    } finally {
    lock.unlock();
    }
    }
    }


    //执行结果
    2019-05-23 18:29:15.153 15531-15593/cn.ycbjie.ycthreadpool I/System.out: 正在执行的是:T1
    2019-05-23 18:29:15.154 15531-15594/cn.ycbjie.ycthreadpool I/System.out: 正在执行的是:T2
    2019-05-23 18:29:15.154 15531-15595/cn.ycbjie.ycthreadpool I/System.out: 正在执行的是:T3