Java多线程学习笔记

1. 引入

在学习多线程之前,我们需要先学习一些基本概念:

多核 CPU 和多 CPU

多核 CPU 是在一枚处理器(CPU)中集成两个或多个完整的计算引擎(核心),不同的核通过 L2 cache 进行通信,存储和外设通过总线与CPU 通信。

多CPU 是多个物理 CPU,CPU 通过总线进行通信,效率比较低。

无论多个计算核是在多个 CPU 芯片上还是在单个 CPU 芯片上,我们称之为多核或多处理器系统。

CPU 核心数和线程数的关系

核心数和线程数是 1:1 的关系,也就是说 4 核的 CPU 可以同时运行 4 个线程。需要注意的是,这里说的同时是指单位时间内可以处理 4 个线程。

英特尔的多线程技术是在CPU内部仅复制必要的资源、让两个线程可同时运行;在一单位时间内处理两个线程的工作,模拟实体双核心、双线程运作。

所以,4 核的 CPU 采用如果采用了超线程技术,那么它可以同时运行 8 个线程。

CPU 时间片轮转调度算法

CPU 时间片轮转调度算法,又叫 RR 调度(Round-Robin,RR),它是专门为分时系统设计的。

在这个算法中,将一个较小的时间单元定义为时间量或时间片。时间片的大小通常是 10~100 ms。就绪队列作为循环队列。CPU 调度程序循环整个就绪队列,为每个进程分配不超过一个时间片的 CPU。

为了实现 RR 调度,我们再次将就绪队列视为进程的 FIFO 队列。新进程添加到就绪队列的尾部。CPU 调度程序从就绪队列中选择第一个进程,将定时器设置在一个时间片后中断,最后分派这个进程。

接下来,有两种情况可能发生。进程可能只需少于时间片的 CPU 执行。对于这种情况,进程本身会自动释放 CPU。调度程序接着处理就绪队列的下一个进程。否则,如果当前运行进程的 CPU 执行大于一个时间片,那么定时器会中断,进而中断操作系统。然后,进行上下文切换(从一个任务切换到另一个任务),再将进程加到就绪队列的尾部(从这点看,RR 调度是抢占式的),接着 CPU 调度程序会选择就绪队列内的下一个进程。

RR 算法的性能很大程度取决于时间片的大小。时间片设置的太短,会导致大量的上下文切换,降低 CPU 效率;时间片设置太长,可能会引起对短的交互请求的响应变差。

关于时间片轮转调度算法可以查看资料: 时间片轮转(RR)调度算法(详解版)。这里介绍地比较详细。

进程和线程

进程是操作系统进行资源分配的最小单位。一个程序至少有一个进程。线程是程序执行的最小单位,进程中的一个负责程序执行的控制单元(执行路径)。一个线程就是在进程中的一个单一的顺序控制流。一个进程至少有一个线程。

进程有自己独立的地址空间,在进程启动时,系统就给它分配了地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此 CPU 切换一个线程的花费远比进程小很多,而且创建一个线程的开销也比进程小很多(这是因为线程基本上不拥有系统资源,只拥有在运行中必不可少的资源,如程序计数器,一组寄存器和栈)。

线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要使用 IPC 接口,包括管道、消息排队、共用内存以及套接字等。

并行和并发

并行(Parallel)是“并排行走”或“同时实行”,在计算机操作系统中指,一组程序按照独立异步的速度执行,无论是微观还是宏观,程序都是一起执行的。
并发(Concurrent),是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

讨论并发的时候一定要加个单位时间,也就是说单位时间内的并发量是多少,离开了单位时间谈论并发是没有意义的。

2. 为什么使用并发编程?

使用并发编程可以发挥多处理器的强大能力

我们可以让操作系统同时运行多个任务,比如一边在浏览器里浏览文章,一边在 Word 里做笔记,还可以一边听着 MP3,这里就有 3 个任务在运行。

使用并发编程可以构建响应更灵敏的用户界面

比如我们在网页上看电影,一边又在下载电影,这里就需要用到两个线程:一个线程用于播放,一个线程用于下载。试想只有一个线程实现的话,就要么先在网页上看电影,等看完后再去下载;要么先下载好,再去网页上看电影。

可能会有同学说,我在一个线程里同时执行播放电影和下载电影不行吗?这样播放电影的界面会非常卡。其实,我们还可以用 Android 手机的应用来说明,我们知道应用里有一个主线程,也叫UI线程,这个线程负责处理用户的操作响应及界面的绘制,现在用户在屏幕上点击按钮下载一个 MP3 文件,这时如果仍在主线程单独去处理这个下载任务,那么用户在屏幕上做点击,滑动等操作会很卡,这非常影响用户体验。所以,这种情况下,Android 会弹出 ANR (应用无响应)的提示框。当然,最佳的做法是开启一个工作线程,单独去处理下载 MP3 文件的任务,等到下载任务完成后,通知主线程,这样用户就可以得知下载已完成。这样的好处,就是保持用户界面灵敏,及时响应。

异步化,模块化代码

比如,我们应用里有登录,数据上报,下载,这些其实都是一个一个的任务,把它们放在单独的线程里来执行,不仅可以使用户界面灵敏,而且实现了模块化。模块化怎么理解呢?比如,登录任务,在公司的几个应用里都用到了登录功能,那么我们把登录做一下封装,提供一些调用接口及回调接口供其他同学使用,这就是模块化。模块化有什么好处?避免了其他同学再去开发一遍,也可以方便地对这个模块进行测试以及定位问题。

3. Java 如何实现并发编程?

实现并发编程的方式有:多进程模式;多线程模式;多进程+多线程模式。

Java语言内置了对多线程的支持:运行一个 Java 程序实际上是在运行一个 JVM 进程,JVM 进程在主线程里执行 main() 方法;在 main() 方法内部,又可以启动多个线程。JVM 还会开启其他工作线程,如垃圾回收线程等。

所以,Java 是采用多线程模式实现并发编程的。

不过,需要特别说明的是,多线程模式下,会出现线程安全问题。

下面的部分包括从开启线程的方式,线程的状态,引出线程的安全问题,解决线程安全问题的一系列方案,线程间通信一一介绍。

4. 开启线程的方式

声明继承 Thread 的类

步骤如下:

  1. 定义一个继承 Thread 的子类;
  2. 在子类中覆盖 Thread 中的 run 方法;
  3. 创建子类对象得到线程对象;
  4. 调用线程对象的 start 方法启动线程。

下面是演示代码:

// 1, 定义一个继承 Thread 的子类
class MyThread extends Thread {
    // 2, 在子类中覆盖 Thread 中的 run 方法
    @Override
    public void run() {
        System.out.println("I am executing a heavy task.");
    }
}
public class Create1 {
    public static void main(String[] args) {
        // 3, 创建子类对象得到线程对象
        Thread myThread = new MyThread();
        // 4, 调用线程对象的 start 方法启动线程
        myThread.start();
    }
}
/**
 打印結果:
 I am executing a heavy task.
 */

声明实现 Runnable 接口的类

步骤如下:

  1. 定义实现 Runnable 接口的类;
  2. 在实现类中覆盖 Runnable 接口的 run 方法;
  3. 通过 Thread 类创建线程对象,把 Runnable 接口的实现类通过 Thread 的构造方法进行传递;
  4. 调用线程对象的 start 方法启动线程。

下面是演示代码:

// 1. 定义实现 Runnable 接口的类;
class MyRunnable implements Runnable {
    // 2. 在实现类中覆盖 Runnable 接口的 run 方法;
    @Override
    public void run() {
        System.out.println("I am executing a heavy task.");
    }
}
public class Create2 {
    public static void main(String[] args) {
        // 3. 通过 Thread 类创建线程对象,把 Runnable 接口的实现类
        // 通过 Thread 的构造方法进行传递;
        Thread thread = new Thread(new MyRunnable());
        // 4. 调用线程对象的 start 方法启动线程。
        thread.start();
    }
}
/*
打印結果:
I am executing a heavy task.
 */

需要注意的地方

一个线程实例只能调用 start 方法一次

调用多次,会抛出如下异常:

Exception in thread "main" java.lang.IllegalThreadStateException

这一点的原因,从 Thread 类的 start 方法源码可以得出来。

不能混淆了 start 方法和 run 方法

  • 它们的所属不同:start 方法是属于 Thread 类的方法,run 方法是属于 Runnable 接口的方法;
  • 它们的定位不同:在 Thread 对象上调用 start 方法才能创建并开启线程,正因为调用了 start 方法,线程才从无到有,从有到启动,它内部调用了 start0() 这个 native 方法,而run 方法仅仅是封装了需要执行的代码,这是一个普通方法本有的作用。
  • 它们的调用不同:在开启线程时,start 方法是需要手动调用来创建并开启线程的,而 线程的run 方法是由虚拟机调用的。

两种开启线程方式的区别

源码上的区别:

  • 继承Thread : 由于子类重写了Thread类的run(), 当调用start()时, 直接找子类的run()方法;
  • 实现Runnable :构造函数中传入了Runnable的引用, 赋值给成员变量Runnable targetstart()调用run()方法时内部判断成员变量Runnable的引用是否为空,不为空编译时看的是Runnablerun(),运行时执行的是子类的 run() 方法。

使用上的区别:

  • 继承Thread类:可以直接使用Thread类中的方法,代码简单;但是如果已经有了父类,就不能用这种方法,这是因为 Java 中的类不支持多继承;
  • 实现 Runnable 接口:将线程的任务从线程的子类中分离出来,进行了单独的封装。按照面向对象的思想将任务封装成了对象。这个是思想的变化。即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,而且接口是可以多实现的,避免了 Java 中单继承的缺点。但是,不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法,代码复杂。

5. 线程的状态


新建状态

在创建了 Thread 对象后的状态,这时只是一个堆内存中的对象而已,不具备任何真正线程的状态。

就绪状态

Thread 对象调用了 start() 方法后进入的状态,这时线程对象具备 CPU 执行资格(具备 CPU 执行资格是指线程对象可以被 CPU 处理,正在处理队列中排队),但是不具备 CPU 执行权(具备 CPU 执行权指的是获取了 CPU 的时间片,正在执行 run() 方法中的代码)。
这是线程从无到有后的状态,就像新生命一样。
这里举个例子来说明 CPU 执行资格和 CPU 执行权:去园区餐厅吃饭需要有园区的餐卡才可以,这时就可以说有卡的员工能够在园区排队打饭,也就是说具备打饭的资格;有卡正在打饭的员工是具备打饭的资格并且具备打饭的执行权;那么,没有卡的外来人员,不能在园区排队打饭,也就是说没有打饭的资格,当然也不可能去打饭,也就是说没有打饭的执行权。
大家一定要理解 CPU 执行资格和 CPU 执行权,因为下面会用它们来区分线程的一些状态。

运行状态

这时线程具备 CPU 的执行资格并且具备 CPU 的执行权,具体来说,线程正处于 CPU 分配的时间片内,执行着 run() 方法里的代码。

从就绪到运行,要么是直接被 CPU 选中,要么是调用了线程对象的 join() 方法而被 CPU 选中。

消亡状态

run() 方法结束的时候,也就是线程任务完成的时候,就自然进入了消亡状态;
当调用了线程对象的 stop()(这个方法已经标记为 @Deprecated,不建议使用的方法) 方法后,好比是因为不可抗力进入消亡状态;
至于 thread.setDaemon(true) 是指的后台线程,调用这句代码需要在线程的 start() 方法之前调用,把线程设置为后台线程。后台线程,是在程序运行时在后台提供一种通用服务的线程,它并不属于程序中不可缺少的部分。因此,当所有的非后台线程结束时,程序就终止了,并且会杀死进程中所有的后台线程。

另外,需要说明一下上面图中提到的方法:

join 方法

 public final synchronized void join(long millis)
    throws InterruptedException

这是 Thread 类的一个成员方法,会抛出 InterruptedException。比如现在有线程 A 和线程 B,A,B 线程已经开启,现在在 A 线程的 run 方法里,调用 B线程的 join() 方法,这时 A 线程就会被挂起,直到 B 线程结束了才恢复。直观地理解就是,B 线程插到 A 线程之前执行。
下面是演示代码:

class Task implements Runnable {
    private Thread joiner;
    public Task(Thread joiner) {
        this.joiner = joiner;
    }
    @Override
    public void run() {
        try {
            joiner.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " is doing task.");
    }
}
public class JoinDemo {
    public static void main(String[] args) {
        Thread joiner = Thread.currentThread();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Task(joiner), "Thread " + i);
            thread.start();
            joiner = thread;
        }
        System.out.println(Thread.currentThread().getName() + " is doing task.");
    }
}

打印结果:

main is doing task.
Thread 0 is doing task.
Thread 1 is doing task.
Thread 2 is doing task.
Thread 3 is doing task.
Thread 4 is doing task.

可以看到,小号线程都是在大号线程前面执行,这不是一种偶然,每次运行都是这样的结果。这是因为调用了 joiner.join() 方法。可以尝试一下,把 joiner.join() 注释掉,打印出来的结果一定不能保证每次都一样。

sleep 方法:

public static native void sleep(long millis) 
	throws InterruptedException;

这是 Thread 类中的一个静态方法,会抛出 InterruptedException。表示使任务中止执行给定的时间。

yield 方法:

public static native void yield();

这是 Thread 类的一个静态方法,没有异常抛出。执行这个方法,表明当前线程已经执行完最重要的任务,现在给线程调度器建议:切换给其他线程执行。

interrupt 方法:

public void interrupt()
public static boolean interrupted()

Thread 类中有一个静态的 interrupted() 方法和一个interrupt() 成员方法。关于它们的不同,后面会做说明。

wait 方法:
Object 类中:

public final void wait() throws InterruptedException
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException

表示等待某个条件发生变化。需要强调一下的是,这个方法是 Object 类中的方法,而不是 Thread 类中的方法。
notify/notifyAll 方法:
Object 类中:

public final native void notify();
public final native void notifyAll();

表示通知条件已发生变化。同样地,需要强调一下的是,这个方法是 Object类中的方法,而不是 Thread 类中的方法。

6. 线程安全问题

6.1 线程安全问题的原因

我们从一个多窗口卖票的例子来做一下说明线程安全问题:
火车站售票大厅有 4 个售票窗口可以售票,现有 10 张票待售。现在考虑一下,使用程序实现这个卖票的过程。

思考一下:10 张票待出售,这是任务,任务在程序里是 Runnable 的实现类;4 个售票窗口是用来执行售票任务,在程序中就是线程。

程序实现如下:

class Ticket implements Runnable {
    private int num = 10;
    @Override
    public void run() {
        while (true) {
            if (num > 0) {
            	// 线程逗留点1
                try {
                	// 售票需要时间,这里给 100 ms。
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 线程逗留点2
                System.out.println(Thread.currentThread().getName() + ".....sell.....Ticket#" + num--);
            }
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket();
        Thread window1 = new Thread(t, "Window1");
        Thread window2 = new Thread(t, "Window2");
        Thread window3 = new Thread(t, "Window3");
        Thread window4 = new Thread(t, "Window4");
        window1.start();
        window2.start();
        window3.start();
        window4.start();
    }
}

运行一下上面的程序,可以发现打印结果并非是每次一样的。
这里以一次的运行结果为例,来说明问题:

Window4.....sell.....Ticket#10
Window1.....sell.....Ticket#8
Window3.....sell.....Ticket#9
Window2.....sell.....Ticket#10
Window1.....sell.....Ticket#7
Window2.....sell.....Ticket#6
Window3.....sell.....Ticket#5
Window4.....sell.....Ticket#4
Window1.....sell.....Ticket#3
Window4.....sell.....Ticket#2
Window3.....sell.....Ticket#1
Window2.....sell.....Ticket#0
Window1.....sell.....Ticket#-1
Window4.....sell.....Ticket#-2

观察上面的打印信息,不难发现一些不对劲儿的地方:
Ticket#10 这张票,被 Window4 和 Window2 各售出一次;更严重的是,竟然卖出了 Ticket#0,Ticker#-1,Ticket#-2 这种根本就不存在的票。

这是什么原因呢?我们就打印日志的最后四行来分析:

Window3.....sell.....Ticket#1
Window2.....sell.....Ticket#0
Window1.....sell.....Ticket#-1
Window4.....sell.....Ticket#-2

run 方法里的 if 语句单独拿出来:

if (num > 0) {
  	 // 线程执行站点1
      try {
      	// 售票需要时间,这里给 100 ms。
          Thread.sleep(100);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      // 线程执行站点2
      System.out.println(Thread.currentThread().getName() + ".....sell.....Ticket#" + num--);
}

为方便说明,在上面的代码中加入了线程站点 1,线程站点 2。它们的含义是线程在这两处可能被给到 CPU 时间片,或者被剥夺 CPU 时间片。
Window3 首先拿到了 CPU 时间片,它执行到了打印语句的地方;与此同时,Window2,Window1,Window4 都到达了线程执行站点1,它们被剥夺了 CPU 时间片。在 Window3 执行完打印语句后,这时 num 的值已经是 0 了;
这时 Window2 获取了 CPU 时间片,继续执行它的代码,打印出 0 号票,这时num 的值已经是-1
之后,Window1 获取了 CPU 时间片,继续执行它的代码,打印出 -1 号票,这时 num 的值已经是 -2 了;
之后,Window4 获取到了 CPU 时间片,继续执行它的代码,打印出 -2 号票。

上面分析了导致结果异常的原因,就是多个线程同时执行了相同的任务,对票数这一数据操作导致的。
试想一下,如果只有一个窗口在卖票,还会出现输出结果异常吗?
我们可以把 Window2,Window3,Window4 这几个窗口关掉,仅留下 Window1,打印一下,结果如下:

Window1.....sell.....Ticket#10
Window1.....sell.....Ticket#9
Window1.....sell.....Ticket#8
Window1.....sell.....Ticket#7
Window1.....sell.....Ticket#6
Window1.....sell.....Ticket#5
Window1.....sell.....Ticket#4
Window1.....sell.....Ticket#3
Window1.....sell.....Ticket#2
Window1.....sell.....Ticket#1

这可不是偶然的结果,因为一个线程执行任务,数据只有它自己在操作,不会出现异常情况。
那么,多线程执行同一个任务就一定会出现问题吗?并不是的,如果多线程所执行的任务只是一行打印语句,当然不会有问题。问题在于任务里面包含了多行代码。
所以,这里总结一下,线程安全问题产生的原因:
第一点,多个线程在操作共享的数据;
第二点,操作共享数据的线程代码有多行。

这两点是且的关系。

6.2 线程安全问题解决办法

如果有一种机制,能使一个线程在操作多行代码时,其他线程不能再去操作这些多行代码;只有当这个线程操作完这些多行代码后,其他线程才能够去操作这些多行代码。这样就能好比把这些多行代码打包成一个整体,就好像“一行代码”一样。这样就不会出现线程安全问题了。
在 Java 中,已经存在这样的机制,这种机制是通过关键字 synchronized 来完成的。

6.2.1 同步代码块

同步代码块的写法如下:

sychronized(对象) {
	需要被同步的代码;
}

其中,sychronized 是一个关键字,后面跟着一个括号里,是对象,起锁的作用,大括号里就是需要同步的代码。这里面关键的是用什么对象来作锁,识别哪些是需要被同步的代码。

Java 通过提供synchronized 关键字的形式,对于防止资源冲突提供了内置的支持。当任务要执行被 synchronized 关键字保护的代码片段的时候,会先检查锁是否可用,可用的话就获取锁,执行代码片段,再释放锁;不可用的话就不能获取锁,也就不能执行代码片段,此时任务处于阻塞状态。

回到卖票的例子里,我们使用一个 new Object(); 对象来作锁,需要同步的代码是:

if (num > 0) {
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + ".....sell.....Ticket#" + num--);
}

完整的代码如下:

class Ticket implements Runnable {
    private int num = 10;
    private Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (num > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ".....sell.....Ticket#" + num--);
                }
            }
        }
    }
}

再次多次运行程序,可以看到输出结果都是正常的。

6.2.2 同步函数

我们把需要同步的多行代码,封装在一个函数 sellTicket 里面,代码就是这样的:

class Ticket implements Runnable {
    private int num = 10;
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            sellTicket();
        }
    }

    private void sellTicket() {
        if (num > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ".....sell.....Ticket#" + num--);
        }
    }
}

注意,目前我们并没有做同步处理,运行时必然是存在线程安全问题的。现在我们对 sellTicket() 里面的代码作同步处理,如下:

private void sellTicket() {
    synchronized (obj) {
        if (num > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ".....sell.....Ticket#" + num--);
        }
    }
}

这样同样解决了线程安全问题。但是,我们注意到,sellTicket 是对需要同步的多行代码进行了封装,而同步代码块同样是对多行代码进行了封装,实现了同步。既然它们都是封装,难道不能合并吗?
可以的,这就是同步函数的写法:

private synchronized void sellTicket() {
    if (num > 0) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ".....sell.....Ticket#" + num--);
    }
}

就是把 sychronized 关键字写在函数声明里面,这和上面同步代码块的写法作用是一样的,同步函数可以说是同步代码写法的简写形式。

6.2.3 静态同步函数

在同步函数的声明上添加 static 关键字,这就是静态同步函数:

private static synchronized void sellTicket() {
    if (num > 0) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ".....sell.....Ticket#" + num--);
    }
}

在本例中,还需要把 num 变量声明为 static 类型。
静态同步函数同样可以达到目的。

6.2.4 同步代码块,同步函数,静态同步函数的区别

既然三者都可以实现同步的目的,那么它们之间有什么区别呢?
它们的区别在于它们持有的锁不一样。
我们知道,同步代码块的锁可以是任意的对象
同步函数使用的锁是 this
静态同步函数使用的锁是该函数所在类的字节码文件对象,即类名.class。

6.2.5 同步的优点和缺点

同步的好处:解决了线程的安全问题;
同步的弊端:相对降低了效率,因为同步外的线程都会判断同步所;
同步的前提:同步中必须有多个线程并使用同一个锁。多个线程需要在同一个锁当中。

6.2.6 死锁的例子

如果此时有一个线程 A,需要按照先获得锁 1 再获得锁 2 的的顺序获得锁,而在此同时又有另外一个线程B,按照先获得锁 2 再锁 1 的顺序获得锁,这种情况就会造成死锁,谁也无法拿到对方的锁。

class TaskA implements Runnable {

    @Override
    public void run() {
        while (true) {
            synchronized (DeadLockDemo.lock1) {
                System.out.println(Thread.currentThread().getName() + " do something.");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (DeadLockDemo.lock2) {
                    System.out.println(Thread.currentThread().getName() + " do other thing.");
                }
            }
        }
    }
}

class TaskB implements Runnable {

    @Override
    public void run() {
        while (true) {
            synchronized (DeadLockDemo.lock2) {
                System.out.println(Thread.currentThread().getName() + " do something.");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (DeadLockDemo.lock1) {
                    System.out.println(Thread.currentThread().getName() + " do other thing.");
                }
            }
        }

    }
}
public class DeadLockDemo {
    public static Object lock1 = new Object();
    public static Object lock2 = new Object();

    public static void main(String[] args) {
        Thread threadA = new Thread(new TaskA(), "ThreadA");
        Thread threadB = new Thread(new TaskB(), "ThreadB");

        threadA.start();
        threadB.start();

    }
}

7. 线程间通信

7.1 单开发单测试的例子

7.1.1 发布 apk /测试 apk 的例子

在软件开发过程中,对 apk 来说有两个过程:一个是开发工程师发布 apk,一个是测试工程师测试 apk。测试 apk 任务在发布 apk 任务完成之前,是不能执行工作的;而发布 apk 任务在发另一个 apk 之前,必须等待测试任务完成。

从面向对象思想的角度,apk 在程序里就是一个对象,所以我们声明 Apk.java,它目前有两个属性,apkName 表示应用名称,versionName 表示版本名称:

class Apk {
   String apkName;
   String versionName;
}

两个过程:开发工程师发布 apk,测试工程师测试 apk,在程序中就是两个不同的任务,即两个不同的 Runnable 实现类。分别命名为 ReleaseApkRunnableTestApkRunnable,它们都是 Runnable 接口的实现类。
虽然任务是不同的,但是它们都要处理同一个 apk,即测试工程师测试的 apk 就是开发工程师发布的 apk。所以,这两个任务拥有共同的资源,即 Apk 对象,声明如下:

Apk apk = new Apk();

在程序中,如何让两个任务共享这个 Apk 对象呢?这里,采用通过任务声明的构造函数把 Apk 对象分别注入到两个任务中。
开发工程师执行发布 apk 的任务,我们需要把这个任务放在 ReleaseApkRunnbalerun 方法里面,代码如下:

class ReleaseApkRunnable implements Runnable {
   private Apk apk;

   public ReleaseApkRunnable(Apk apk) {
       this.apk = apk;
   }

   @Override
   public void run() {
       int x = 0;
       while (true) {
           try {
               // 这 200 ms 当作开发时间
               TimeUnit.MILLISECONDS.sleep(200);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           if (x % 2 == 0) {
               apk.apkName = "QQ";
               apk.versionName = "Overseas";
           } else {
               apk.apkName = "微信";
               apk.versionName = "国内版";
           }
           System.out.println("Release apk: "  + apk.apkName + "," + apk.versionName );
           x++;
       }
   }
}

可以看到上面的代码,通过构造函数传参的方式,把 Apk 对象这个共同的资源传递进来;在 run 方法里,就是开发工程师发布 apk 的任务代码:首先,使用 200 ms 的休眠代表开发时间,然后就开始打包,这里有两种包:QQ 的 Overseas 版,微信的国内版,最后发布。
我们使用了一个 int x 来保证开发工程师发布的包是按照 QQ 的 Overseas 版,微信的国内版这样的顺序一个一个发布的。这一点是比较好理解的。

需要注意的是,上述的发布过程包含在一个 while 无限循环里。

测试工程师执行测试 apk 的任务,同样地需要把测试 apk 的执行代码放到 TestApkRunnablerun 方法中:

class TestApkRunnable implements Runnable {
    private Apk apk;

    public TestApkRunnable(Apk apk) {
        this.apk = apk;
    }

    @Override
    public void run() {
        while (true) {
            try {
                // 这 60 ms 作为测试时间
                TimeUnit.MILLISECONDS.sleep(60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Test pass: "  + apk.apkName + "," + apk.versionName );
        }
    }
}

同样地,在上面的代码中,通过构造函数传参的方式,把 Apk 对象传递给 TestApkRunnable 类,在 run 方法内部,就是执行测试任务的代码:首先,休眠 60ms 的时间作为测试时间,之后,就发出测试结果,这里都作测试通过处理。
同样地,测试 apk 的过程包含在一个 while 无限循环里面。

要运行ReleaseApkRunnableTestApkRunnable 这两个任务,需要创建两个线程,releaseThreadtestThread,并调用它们的 start 方法。测试代码如下:

public class ApkDemo {
    public static void main(String[] args) {
        Apk apk = new Apk();
        Runnable releaseApkRunnable = new ReleaseApkRunnable(apk);
        Runnable testApkRunnable = new TestApkRunnable(apk);
        Thread releaseThread = new Thread(releaseApkRunnable);
        Thread testThread = new Thread(testApkRunnable);
        releaseThread.start();
        testThread.start();
    }
}

运行程序后,其中一次的打印结果如下(这里只是截取一段日志):

Test pass: null,null
Test pass: null,null
Test pass: null,null
Release apk: QQ,Overseas
Test pass: QQ,Overseas
Test pass: QQ,Overseas
Test pass: QQ,Overseas
Release apk: 微信,国内版
Test pass: 微信,国内版
Test pass: 微信,国内版
Test pass: 微信,国内版

从打印结果里,我们看到:
在没有任何 Apk 信息的情况下,测试工程师首先就开始了 3 次测试,也就是说,开发工程师还没有帆布 Apk 包,测试工程师就进行了 3 次测试,这肯定是不对的。
开发工程师发布了一个 QQ,Overseas 的 Apk 包,测试工程师居然进行了 3 次测试 QQ, Overseas 包的过程,这也是不对的。因为,我们的设定是一次测试通过,不存在这种多次测试一个包的情况。

7.1.2 解决数据错乱问题

还有一个问题,上面的例子没有跑出来,就是由于线程不安全造成数据错乱的问题。

回顾一下,线程安全问题产生的条件:
第一,多条线程操作共享数据;
第二,共享数据里包含多条执行代码。

看一下我们的代码,ReleaseApkRunnableTestApkRunnable 都在操作共享数据 Apk 对象,满足第一条;对共享数据的处理,包括给 apkNameversionName 赋值,以及打印语句,这里面包含了多行执行代码,满足第二条。所以,这个例子也是有线程安全问题。

这里通过把 ReleaseApkRunnableTestApkRunnable 稍作修改,来验证存在线程安全问题:

class ReleaseApkRunnable implements Runnable {
    private Apk apk;

    public ReleaseApkRunnable(Apk apk) {
        this.apk = apk;
    }

    @Override
    public void run() {
        int x = 0;
        while (true) {
            if (x % 2 == 0) {
                apk.apkName = "QQ";
                apk.versionName = "Overseas";
            } else {
                apk.apkName = "微信";
                apk.versionName = "国内版";
            }
            x++;
        }
    }
}

class TestApkRunnable implements Runnable {
    private Apk apk;

    public TestApkRunnable(Apk apk) {
        this.apk = apk;
    }

    @Override
    public void run() {
        while (true) {
            System.out.println("Test pass: "  + apk.apkName + "," + apk.versionName );
        }
    }
}

截取一段打印结果如下:

Test pass: QQ,国内版
Test pass: QQ,Overseas
Test pass: QQ,Overseas
Test pass: 微信,国内版
Test pass: 微信,国内版
Test pass: 微信,Overseas

这就证明了我们的例子确实存在线程安全问题。

解决线程安全问题,这里采用同步代码块。

ReleaseApkRunnable 中的 run 方法调整如下:

public void run() {
    int x = 0;
    while (true) {
        synchronized (apk) {
            try {
                // 这 200 ms 当作开发时间
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (x % 2 == 0) {
                apk.apkName = "QQ";
                apk.versionName = "Overseas";
            } else {
                apk.apkName = "微信";
                apk.versionName = "国内版";
            }
            System.out.println("Release apk: "  + apk.apkName + "," + apk.versionName );
        }
        x++;
    }
}

TestApkRunnable 中的 run 方法调整如下:

public void run() {
    while (true) {
        synchronized (apk) {
            try {
                // 这 60 ms 作为测试时间
                TimeUnit.MILLISECONDS.sleep(75);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Test pass: "  + apk.apkName + "," + apk.versionName );
        }
    }
}

需要特别注意的是两者使用的是同一个锁,即 Apk 对象。这样才能保证同步。可以测试,如果两者使用的是不同的锁,一定不能保证同步,也就是说一定还存在线程安全问题。

解决了线程安全问题之后,回到打印结果不正常的问题,这样的结果是如何造成的呢?

可以回顾上面的代码,开发工程师发布 apk 的任务就是不停地按照一个 QQ,Overseas版,一个微信,国内版,这样的次序不停地在发布 apk;而测试工程师测试 apk 的任务就是不停地测试通过。这两个任务彼此互不关心:开发工程师不管测试工程师是不是测试 apk 完毕,只管自己发布 apk;测试工程师也不去看看开发工程师有没有待测的 apk,只管傻乎乎地去执行测试。

它们之间没有任何沟通,没有协作造成了输出结果不正常。

如何解决上面提到的问题呢?

7.1.3 尝试解决线程不协作的问题

可以想到实际的工作中,是不会出现这些问题的。因为开发工程师总是在确认测试工程师测试 apk 完毕后才会发布另一个 apk;测试工程师也会在知道有待测的 apk 的情况下,才会去执行测试。

这里我们给 Apk 对象添加一个字段:boolean isForTest;

class Apk {
    String apkName;
    String versionName;
    // 新增加的字段,默认是 false
    boolean isForTest = false; 
}

当开发工程师确认 isForTestfalse时,表示测试工程师没有测试 apk,这时就会发布 apk,并且把 isForTest 设置为 true,表示交付测试了;如果 isForTesttrue 时,表示测试工程师有测试 apk,开发工程师就不再执行发布 apk 的代码。

当测试工程师确认 isForTesttrue 时,表示现在有 apk 需要测试,就会执行测试 apk 代码;如果 isForTestfalse,表示现在没有 apk 需要测试,就不执行测试 apk 的代码。

上面就是我们解决问题的思路,下面看代码实现:

修改 ReleaseApkRunnable 中的 run 方法如下:

public void run() {
    int x = 0;
    while (true) {
        synchronized (apk) {
            if (apk.isForTest) {
            	// 测试工程师有apk在测试,不执行发布apk的代码
                continue;
            }
            try {
                // 这 200 ms 当作开发时间
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (x % 2 == 0) {
                apk.apkName = "QQ";
                apk.versionName = "Overseas";
            } else {
                apk.apkName = "微信";
                apk.versionName = "国内版";
            }
            System.out.println("Release apk: "  + apk.apkName + "," + apk.versionName );
            // 发布apk后,设置 isForTest 为 true,表示交付测试。
            apk.isForTest = true;
        }
        x++;
    }
}

修改 TestApkRunnablerun 方法如下:

public void run() {
    while (true) {
        synchronized (apk) {
            if (!apk.isForTest) {
            	// 没有 apk 要测试,不执行下面测试 apk 的代码。
                continue;
            }
            try {
                // 这 60 ms 作为测试时间
                TimeUnit.MILLISECONDS.sleep(60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 测试完毕,把 isForTest 改为 false,表明手头没有要测试的 apk。
            apk.isForTest = false;
            System.out.println("Test pass: "  + apk.apkName + "," + apk.versionName );
        }
    }
}

好了,多次执行测试代码,都可以得到正确的结果,如下面的截取日志:

Release apk: QQ,Overseas
Test pass: QQ,Overseas
Release apk: 微信,国内版
Test pass: 微信,国内版

虽然打印输出是正确的,但是程序本身还是存在很大的问题。

回顾一下代码,开发工程师在判断有测试 apk 的标记 isForTesttrue 时,是采用 continue 的方式跳过发布 apk 的代码,继续下一轮循环;而测试工程师在判断有要测试 apk 的标记 isForTestfalse 时,同样采用 continue 的方式跳过测试 apk 的代码,继续下一轮循环。
如果开发工程师一直不发布 apk,那么测试工程师就要一直去循环:检查 isForTest 的标记何时为 true,以便去执行测试的代码;如果测试工程师一直有 apk 待测,那么开发工程师就要一直去循环:检查 isForTest 的标记何时为 false,以便去执行发布 apk 的代码。

这显然是不正常的,开发工程师不可能不停地查看测试工程师是否测试完毕,测试工程师也不会一直询问开发工程师发布 apk 了没有。这样的话,等于在浪费时间。对于程序来说这会消耗宝贵的资源:它们都处于运行状态,它们都需要 CPU 分配时间片。

我们知道,实际的工作中的情况是这样的:

开发工程师在注意到测试工程师还有 apk 测试时,就知道这时不必去发布 apk,这时等着就行了,如果测试工程师没有 apk 在测试,那么就发布 一个 apk,并通知测试工程师:新的 apk 已发布,请测试。这时,测试工程师收到通知,就开始执行测试 apk 的过程。

测试工程师在注意到开发工程师还没有 apk 发布时,就知道这时不必去测试 apk,这时等着就行了;如果开发工程师发布了 apk,就去测试 apk,并通知开发工程师:apk 已测试完毕,测试通过。这时,开发工程师收到通知,就开始执行发布 apk 的过程。

那么,用程序如何实现呢?这就需要等待/唤醒机制。

7.2 等待/唤醒机制

7.2.1 初步代码实现

在 Java 中,有对应的实现:

Object 类中的 wait() 方法:调用对象的 wait() 方法时,当前线程被挂起,而锁会被释放。在其他线程调用此对象的 notify() 方法或notifyAll() 方法前,当前线程就会处于等待状态。这时线程释放了 CPU 执行权,并释放了 CPU 执行资格。

Object 类中的 notify() 方法:唤醒在对对象的 wait() 方法的调用中被挂起的任务。如果有多个任务在此对象上等待,则会选择唤醒一个任务。被唤醒的任务就具备了 CPU 执行资格。

需要注意的是,如果当前线程不是对象监视器的所有者,那么调用对象监视器的 wait(),或notify() 方法会抛出 IllegalMonitorStateException。这个异常的含义是当一个线程本身不持有指定的对象监视器,却试图在这个对象监视器上等待,或者通知其他在这个对象监视器上等待的线程,这时就会抛出这个异常。换句话说,只有当前线程是对象监视器的所有者时,调用对象监视器的 wait()notify() 方法才不会抛出 IllegalMonitorStateException。所以,我们必须在同步中,调用对象的 wait()notify() 方法。

我们来看代码实现:

修改 ReleaseApkRunnablerun 方法如下:

public void run() {
    int x = 0;
    while (true) {
        synchronized (apk) {
            if (apk.isForTest) {
            	// 把 continue; 替换为 apk.wait();
                try {
                    apk.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                // 这 200 ms 当作开发时间
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (x % 2 == 0) {
                apk.apkName = "QQ";
                apk.versionName = "Overseas";
            } else {
                apk.apkName = "微信";
                apk.versionName = "国内版";
            }
            apk.isForTest = true;
            System.out.println("Release apk: "  + apk.apkName + "," + apk.versionName );
            // 添加了通知,唤醒方法的调用
            apk.notify();
        }
            x++;
    }
}

修改的地方有两处:一是把之前例子中的 continue; 替换为了 apk.wait(),二是在同步代码块的最后一行,添加 apk.notify()

解释一下改动的含义:

if 语句判断有 apk 在测试时,就会进入 if 分支调用 apk.wait() ,这时开发工程师线程会被挂起,而 apk 这个锁对象会被开发工程师线程释放。开发工程师线程会一直等待,直到测试工程师通知他需要再发包为止。

当开发工程师执行完发包任务后,就调用 apk.notify() 方法,这时就会通知在等待测试 apk 的测试工程师:新的 apk 已发布,请测试。这将通知在对 wait() 的调用中被挂起的测试工程师线程继续工作。在等待中的测试工程师就会收到通知继续工作前,必须重新获得之前因为调用 apk.wait() 时释放的锁。

修改 TestApkRunnablerun 方法如下:

public void run() {
    while (true) {
        synchronized (apk) {
            if (!apk.isForTest) {
            	// 把 continue; 替换为 apk.wait();
                try {
                    apk.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                // 这 60 ms 作为测试时间
                TimeUnit.MILLISECONDS.sleep(60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Test pass: "  + apk.apkName + "," + apk.versionName );
            apk.isForTest = false;
            // 添加了通知,唤醒方法的调用
            apk.notify();
        }
    }
}

修改的地方仍是两处:一是把之前例子中的 continue; 替换为了 apk.wait(),二是在同步代码块的最后一行,添加 apk.notify()

解释一下改动的含义:

if 语句判断没有 apk 需要测试时,就会进入 if 分支调用 apk.wait() ,这时测试工程师线程会被挂起,而 apk 这个锁对象会被测试工程师线程释放。测试工程师线程会一直等待,直到开发工程师通知他有新包要测试为止。

当测试工程师执行完测试任务后,就调用 apk.notify() 方法,这时就会通知在等待发布 apk 的开发工程师:apk 已测试完毕,测试通过。这将通知在对 wait() 的调用中被挂起的开发工程师线程继续工作。在等待中的开发工程师就会收到通知继续工作前,必须重新获得之前因为调用 apk.wait() 时释放的锁。

运行一下程序,结果是符合预期的。

总结一下,采用了等待/唤醒机制的例子与之前 7.1.3 中的实现相比,不再依靠循环来决定开发工程师何时发包,测试工程师何时测试,这会减少对 CPU 的无效占用。

7.2.2 优化后的代码实现

对 7.2.1 中的实现,优化为同步函数的实现,这也是实际开发中的写法。其实,就是进行了封装而已。这样的好处是,可以实现同步代码的复用。

class Apk {
    private String apkName;
    private String versionName;
    private boolean isForTest = false;

    public synchronized void releaseApk(String apkName, String versionName) {
        if (isForTest) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            // 这 200 ms 当作开发时间
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.apkName = apkName;
        this.versionName = versionName;
        isForTest = true;
        System.out.println("Release apk: " + this);
        notify();
    }

    public synchronized void testApk() {
        if (!isForTest) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            // 这 60 ms 作为测试时间
            TimeUnit.MILLISECONDS.sleep(60);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isForTest = false;
        System.out.println("Test Apk: " + this);
        notify();
    }

    @Override
    public String toString() {
        return apkName + "," + versionName;
    }
}

class ReleaseApkRunnable implements Runnable {
    private Apk apk;

    public ReleaseApkRunnable(Apk apk) {
        this.apk = apk;
    }

    @Override
    public void run() {
        int x = 0;
        while (true) {
            if (x % 2 == 0) {
                apk.releaseApk("QQ", "Overseas");
            } else {
                apk.releaseApk("微信", "国内版");
            }
            x++;
        }

    }
}

class TestApkRunnable implements Runnable {
    private Apk apk;

    public TestApkRunnable(Apk apk) {
        this.apk = apk;
    }

    @Override
    public void run() {
        while (true) {
            apk.testApk();
        }
    }
}

7.3 多开发多测试的例子

7.3.1 例子

由于公司业务的发展,一个开发加一个测试难以支撑,所以公司就新招一名开发以及一名测试。现在,有两名开发工程师,两名测试工程师。他们组成新的团队,共同完成任务。

首先是在 main() 方法里,增加了一条开发工程师线程,以及一条测试工程师线程,代码如下:

Thread releaseThread1 = new Thread(releaseApkRunnable, "releaseThread1");
Thread releaseThread2 = new Thread(releaseApkRunnable, "releaseThread2");
Thread testThread1 = new Thread(testApkRunnable, "testThread1");
Thread testThread2 = new Thread(testApkRunnable, "testThread2");
releaseThread1.start();
releaseThread2.start();
testThread1.start();
testThread2.start();

可以看到,这里通过 Thread 类的构造方法设置了线程的名字:releaseThread1releaseThread2testThread1testThread2。这样设置后,通过 Thread.currentThread().getName() 获取到的就是我们设置的名字,这样可读性更好。

其次,简化了 ReleaseApkRunnablerun 方法发布 QQ 应用:

while (true) {
    apk.releaseApk("QQ");
}

最后,在 Apk 类中,增加 code 字段,表示版本号,每次发布新包,版本号都会在原来的基础上加 1,代码如下:

public synchronized void releaseApk(String name) {
    if (isForTest) {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    try {
        // 这 200 ms 当作开发时间
        TimeUnit.MILLISECONDS.sleep(200);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    this.apkName = name +"-V"+ code;
    System.out.println(Thread.currentThread().getName() + "============>Release apk: " + this.apkName);
    code++;
    isForTest = true;
    notify();
}

完整代码如下:

class Apk {
    private String apkName;
    private boolean isForTest = false;
    private int code = 1;
    public synchronized void releaseApk(String name) {
        if (isForTest) {
            try {
                wait(); // rt1
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            // 这 200 ms 当作开发时间
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.apkName = name +"-V"+ code;
        System.out.println(Thread.currentThread().getName() + "============>Release apk: " + this.apkName);
        code++;
        isForTest = true;
        notify();
    }

    public synchronized void testApk() {
        if (!isForTest) {
            try {
                wait(); // tt2, tt1
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            // 这 60 ms 作为测试时间
            TimeUnit.MILLISECONDS.sleep(60);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " Test Apk: " + this.apkName);
        isForTest = false;
        notify();
    }
}

class ReleaseApkRunnable implements Runnable {
    private Apk apk;

    public ReleaseApkRunnable(Apk apk) {
        this.apk = apk;
    }

    @Override
    public void run() {
        while (true) {
            apk.releaseApk("QQ");
        }
    }
}

class TestApkRunnable implements Runnable {
    private Apk apk;

    public TestApkRunnable(Apk apk) {
        this.apk = apk;
    }

    @Override
    public void run() {
        while (true) {
            apk.testApk();
        }
    }
}

public class ApkDemo {
    public static void main(String[] args) {
        Apk apk = new Apk();
        Runnable releaseApkRunnable = new ReleaseApkRunnable(apk);
        Runnable testApkRunnable = new TestApkRunnable(apk);
        Thread releaseThread1 = new Thread(releaseApkRunnable, "releaseThread1");
        Thread releaseThread2 = new Thread(releaseApkRunnable, "releaseThread2");
        Thread testThread1 = new Thread(testApkRunnable, "testThread1");
        Thread testThread2 = new Thread(testApkRunnable, "testThread2");
        releaseThread1.start();
        releaseThread2.start();
        testThread1.start();
        testThread2.start();
    }
}

人员增加后,工作流程还是一样的:开发工程师先发布 apk,测试工程师才能测试 apk;测试工程师测试 apk 完毕后,开发工程师才能继续发布 apk。

7.3.2 分析打印输出不正确的问题

运行一下代码,查看现在是否还可以达到预期的效果。

这里取出刚开始的一段打印日志,大家看一下:

releaseThread1============>Release apk: QQ-V1
testThread2 Test Apk: QQ-V1
releaseThread2============>Release apk: QQ-V2
testThread2 Test Apk: QQ-V2
testThread1 Test Apk: QQ-V2
testThread2 Test Apk: QQ-V2
testThread1 Test Apk: QQ-V2
testThread2 Test Apk: QQ-V2
releaseThread2============>Release apk: QQ-V3
releaseThread1============>Release apk: QQ-V4
testThread1 Test Apk: QQ-V4

这段日志里就包含了两个问题:

一,发布了一个版本,测试了两轮

releaseThread1============>Release apk: QQ-V1
testThread2 Test Apk: QQ-V1
releaseThread2============>Release apk: QQ-V2
testThread2 Test Apk: QQ-V2
testThread1 Test Apk: QQ-V2
testThread2 Test Apk: QQ-V2
testThread1 Test Apk: QQ-V2
testThread2 Test Apk: QQ-V2

二,漏测试版本

releaseThread2============>Release apk: QQ-V3
releaseThread1============>Release apk: QQ-V4
testThread1 Test Apk: QQ-V4

这两个问题都是十分严重的,严重违反了工作流程。

但是,我们的代码确实做了同步处理,也使用等待/唤醒机制。为什么在增加了一个开发,一个测试后,就出问题了呢?

有问题看日志,所以我们认真分析一下日志。

先看第一段问题日志:

releaseThread1============>Release apk: QQ-V1
testThread2 Test Apk: QQ-V1
releaseThread2============>Release apk: QQ-V2
testThread2 Test Apk: QQ-V2
testThread1 Test Apk: QQ-V2
testThread2 Test Apk: QQ-V2
testThread1 Test Apk: QQ-V2
testThread2 Test Apk: QQ-V2

下面分析一下流程,步骤有些多,大家耐心一些啊:

应用刚启动,releaseThread1 就获取锁,这时 isForTestfalse,不会进入 if (isForTest) 里面,继续执行发布 apk 的代码,打印出 releaseThread1============>Release apk: QQ-V1 这行日志,修改 isForTest 的标记为 true,然后调用了 notify() 方法,不过这时等待队列中没有线程,之后,releaseThread1 就结束任务执行,自动释放了锁。

接着,testThread2 获取到了锁,开始执行同步方法里的代码:首先判断 if(!isForTest) 为 false(因为 releaseThread1 里将 isForTest 改为 true!isForTestfalse),不会进入 if 分支,继续执行测试 apk 的代码,打印出 testThread2 Test Apk: QQ-V1,修改 isForTest 标记为 false,最后调用 notify() 方法,这时等待队列中没有线程,之后,testThread2 就结束了测试任务,自动释放了锁。

接着,testThread1 获取到了锁,开始执行同步方法里的代码,因为此时 isForTestfalse,很快就调用锁的 wait() 方法,这样 testThread1 就被挂起,进入线程等待队列,并且释放了锁。这时,等待队列中是 testThread1

接着,testThread2 获取到了锁,开始执行同步方法里的代码,因为此时 isForTestfalse,很快就调用锁的 wait() 方法,这样 testThread2 就被挂起,进入线程等待队列,并且释放了锁。这时,等待队列中有 testThread1testThread2

接着,releaseThread2 获取到了锁,这时 isForTestfalse,继续执行发布 apk 的任务,打印出 releaseThread2============>Release apk: QQ-V2,将 isForTest 标记改为 true,调用 nofity() 方法,唤醒等待队列中的一个线程。现在,等待队列中有 testThread1testThread2。选中 testThread2 唤醒,testThread2 就具备了 CPU 执行资格。现在等待队列中是 testThread1

接着,testThread2 获取到锁,isForTesttrue,继续执行测试 apk 的代码,打印出 testThread2 Test Apk: QQ-V2,把 isForTest改为 false,调用 notify() 方法,唤醒等待队列中的一个线程。现在等待队列中是 testThread1testThread1 被唤醒,具备了 CPU 执行资格。这时,等待队列中没有线程。

接着,testThread2 获取到锁,这时 isForTestfalsetestThread2 调用锁的 wait() 方法,进入等待线程队列。这时等待队列中是 testThread2

接着,testThread1 获取到锁,从被唤醒的地方开始往下执行,打印 testThread1 Test Apk: QQ-V2,把 isForTest 改为 false,调用 notify()方法。这时等待队列中是 testThread2,它被唤醒,具有 CPU 执行资格。当前等待队列中没有线程。

接着,testThread1 获取到锁,这时 isForTestfalse,很快 testThread1 又进入了等待队列,并释放锁。这时等待队列中是 testThread1

接着,testThread2 获取到锁,从被唤醒的地方开始往下执行,打印 testThread2 Test Apk: QQ-V2,把 isForTest 改为 false,调用 notify()方法。

到这里,我们看到 testThread1testThread2 轮流执行了测试 apk 的代码,全然不去理会 isForTestfalse 这一判断条件。这是因为它们都是从被唤醒的地方开始往下执行的,不会再去判断 if(!isForTest)

同样地,可以去分析第二段问题日志:

releaseThread2============>Release apk: QQ-V3
releaseThread1============>Release apk: QQ-V4
testThread1 Test Apk: QQ-V4

是由于没有再去判断 if(isForTest) 导致的。

那么,怎样才能多次回去判断 !isForTestisForResult这两个条件呢?自然是while循环。

现在,releaseApkRunnable 中的 iftestApkRunnable 中的 if 都改为 while,如下:

while (!isForTest) {
    try {
        wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

7.3.3 分析死锁问题

再次运行程序,发现出现了死锁。
其中一次的日志如下:

releaseThread1============>Release apk: QQ-V1
testThread2 Test Apk: QQ-V1
releaseThread2============>Release apk: QQ-V2
testThread2 Test Apk: QQ-V2
releaseThread1============>Release apk: QQ-V3
|(这一行是光标在闪动)

我们来分析一下原因:
分析的思路还是根据日志,理流程。

程序启动后,releaseThread1 获取到锁, isForTestfalse,执行发布 apk 的代码,打印出 releaseThread1============>Release apk: QQ-V1,把 isForTest 修改为 true,调用 notify() 方法,没有需要唤醒的线程,任务结束,释放锁。
接着,releaseThread1 再次获取到锁,isForTesttrue,调用锁的 wait() 方法,releaseThread1 线程被挂起,进入等待队列,并释放锁。这时等待队列里是 releaseThread1

接着,releaseThread2 获取到锁,isForTesttrue,调用锁的 wait() 方法,releaseThread2 线程被挂起,进入等待队列,并释放锁。这时等待队列里是 releaseThread1releaseThread2

接着,testThread2 获取锁,isForTesttrue,开始执行测试 apk 的代码,打印 testThread2 Test Apk: QQ-V1,修改 isForTestfalse,调用 notify() 方法,唤醒等待队列中的一个线程。等待队列中现在是 releaseThread1releaseThread2。选中唤醒 releaseThread2,它具有 CPU 执行资格。等待队列中现在只有 releaseThread1

接着,testThread1 获取到锁,isForTestfalse,调用锁的 wait() 方法,testThread1 被挂起,进入等待队列,并释放锁。这时等待队列有: releaseThread1testThread1

接着,testThread2 获取到锁,isForTestfalse,调用锁的 wait() 方法,testThread2 被挂起,进入等待队列,并释放锁。这时等待队列有: releaseThread1testThread1testThread2

接着,releaseThread2 获取到锁, isForTestfalse,执行发布 apk 的代码,打印出 releaseThread2============>Release apk: QQ-V2,把 isForTest 修改为 true,调用 notify() 方法,唤醒 testThread2,任务结束,释放锁。目前,等待队列中:releaseThread1testThread1

接着,releaseThread2 获取到锁,isForTesttrue,调用锁的 wait() 方法,releaseThread2 线程被挂起,进入等待队列,并释放锁。这时等待队列里是 releaseThread1testThread1releaseThread2

接着,testThread2 获取锁,isForTesttrue,开始执行测试 apk 的代码,打印 testThread2 Test Apk: QQ-V2,修改 isForTestfalse,调用 notify() 方法,唤醒等待队列中的一个线程。等待队列中现在是 releaseThread1testThread1releaseThread2。选中唤醒 releaseThread1,它具有 CPU 执行资格。等待队列中现在只有 ,testThread1releaseThread2

接着,testThread2 获取到锁,isForTestfalse,调用锁的 wait() 方法,testThread2 被挂起,进入等待队列,并释放锁。这时等待队列有: testThread1releaseThread2testThread2

接着,releaseThread1 获取到锁, isForTestfalse,执行发布 apk 的代码,打印出 releaseThread1============>Release apk: QQ-V3,把 isForTest 修改为 true,调用 notify() 方法,唤醒 releaseThread2,任务结束,释放锁。目前,等待队列中:testThread1testThread2

接着,releaseThread2 获取到锁,isForTesttrue,调用锁的 wait() 方法,releaseThread2 线程被挂起,进入等待队列,并释放锁。这时等待队列里是testThread1testThread2releaseThread2

最后,releaseThread1 获取到锁,isForTesttrue,调用锁的 wait() 方法,releaseThread1 线程被挂起,进入等待队列,并释放锁。这时等待队列里是testThread1testThread2releaseThread2releaseThread1

到这里,四个线程都在等待队列中了。这就造成了死锁。
我们看关键的倒数第三步,当时等待队列中是 testThread1releaseThread2testThread2,调用锁的 notify() 方法后,本该唤醒 testThread1testThread2 中的一个,但是却唤醒了 releaseThread2。之后,就最终都进入了等待队列。

为什么没有去唤醒 testThread1testThread2 中的一个,而去唤醒了 releaseThread2 呢?

这是因为调用锁的 notify() 方法,当线程等待队列中有多个时,会选择其中一个唤醒,而选择是随机的,任意性的。

好吧。

问:能不能指定唤醒呢?再不济,全部唤醒也可以啊。
答:指定唤醒目前还没有,全部唤醒可以用 notifyAll()notifyAll() 可以唤醒所有的等待线程。

把代码中的 notify() 替换为 notifyAll() 方法重新测试,打印输出符合流程要求了。截取一段如下:

releaseThread2============>Release apk: QQ-V270
testThread2 Test Apk: QQ-V270
releaseThread1============>Release apk: QQ-V271
testThread2 Test Apk: QQ-V271
releaseThread2============>Release apk: QQ-V272
testThread1 Test Apk: QQ-V272
releaseThread2============>Release apk: QQ-V273
testThread2 Test Apk: QQ-V273
releaseThread1============>Release apk: QQ-V274
testThread2 Test Apk: QQ-V274

7.3.4 如何实现指定唤醒?

这需要借助 Java SE5 的 java.util.concurrent 类库,这里面包含定义在 java.util.concurrent.locks 中的显式的互斥机制。

我们知道,之前使用的synchronized 是一种隐式的互斥机制。

它们之间有什么区别呢?

Lock 对象必须被显式地创建、锁定和释放;而使用 synchronized 这样的内建锁,创建锁,获取锁,释放锁都不需要手动调用。从这一点来看,Lock 对象的形式,代码要比使用 synchronized 关键字时,需要写更多的代码,缺乏优雅性。

使用 synchronized 关键字的形式,包括同步代码块以及方法,它们仅仅是对代码进行封装,仅仅停留在代码块封装,方法封装这个概念上;而 Java SE5 中的 Lock 将同步和锁封装成了对象,包含了创建锁,获取锁,释放锁这些行为。从这里,也就将使用 synchronized 关键字形式的内置锁变为显式的,它可以显式地创建锁,获取锁以及释放锁。Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法(waitnotifynotifyAll)的使用。

Lock 对象不使用块结构,这样失去了使用 synchronized 的方法和代码块的自动释放锁的功能,所以使用 Lock 对象时,必须把把释放锁的操作(unlock())放在 try - finally 语句的 finally 子句中。如下:

Lock l = ...; 
l.lock();
try {
    // access the resource protected by this lock
} finally {
    l.unlock();
}

好了,它们之间的区别先对比到这里。更多的不同之处,我们会通过代码来演示。

注意到,java.util.concurrent.locks 下的 Condition 替代了 Object 类的监视器方法(waitnotifynotifyAll)的使用。那么,具体是怎么替代的呢?

Condition 接口中有 signal() 方法,唤醒一个等待的线程,这可以和 Object 类中的notify()相对应;await() 方法,挂起一个任务,这可以和 Object 类中的 wait() 方法相对应;signalAll() 方法,唤醒所有等待线程,这可以和 Object 类中的 notifyAll() 方法相对应。

注意,说相对应,并不是等同。一个 Lock 对象可以有多个 Condition 对象,也就相应地有多组 await()signal()signalAll() 方法,而使用 synchronized 关键字形式,只能对应一个锁对象,也仅仅有一组 wait()notify()notifyAll() 方法。通过调用 Condition 对象的 signal()signalAll() 方法来唤醒任务,唤醒的是被这个 Condition 对象自身所挂起的任务。

下面,我们准备把 7.3.3 中最后的例子使用 Lock 对象来实现:

class Apk {
    private String apkName;
    private boolean isForTest = false;
    private int code = 1;
    // 创建一个锁对象
    private Lock lock = new ReentrantLock();
    // 在 lock 对象上获取 Condition 实例
    private Condition condition = lock.newCondition();
    
	// 去掉了方法声明中的 synchronized 关键字
    public void releaseApk(String name) {
    	// 替换了原来的 synchronized 的同步方法自动获取锁
        lock.lock();
        try {
            while (isForTest) {
                try {
                	// 替换了原来的 apk.wait()
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                // 这 200 ms 当作开发时间
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.apkName = name +"-V"+ code;
            System.out.println(Thread.currentThread().getName() + "============>Release apk: " + this.apkName);
            code++;
            isForTest = true;
            // 替换了原来的 apk.notifyAll();
            condition.signalAll();
        } finally {
        	// 替换了原来的 synchronized 的同步方法自动释放锁
            lock.unlock();
        }

    }
	// 去掉了方法声明中的 synchronized 关键字
    public void testApk() {
        // 替换了原来的 synchronized 的同步方法自动获取锁
        lock.lock();
        try {
            while (!isForTest) {
                try {
                	// 替换了原来的 apk.wait();
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                // 这 60 ms 作为测试时间
                TimeUnit.MILLISECONDS.sleep(60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " Test Apk: " + this.apkName);
            isForTest = false;
            // 替换了原来的 apk.signalAll();
            condition.signalAll();
        } finally {
        	// 替换了原来的 synchronized 的同步方法自动释放锁
            lock.unlock();
        }
    }
}

上面的代码注释已经很详细了,把进行替换的地方一一进行了注释说明。

这时,执行代码,程序依然能够达到预期的输出。

同样地,可以测验:替换 7.3.2 的例子和 7.3.3 的死锁例子,也可以复现问题。大家可以自己实测一下。

到这里,大家可能会想:LockCondition 的作用也不过如此,之前使用 synchronized 同步方法不也一样实现吗?

目前看来,是这样的。后面会说到 LockConditionsynchronized 方式灵活,强大的地方。

到这里,我们证明了:Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。

在对比 Locksynchronized 的区别时,我们知道,一个 Lock 可以绑定多个 Condition 对象。每个 Condition 对象拥有一组监听器方法:await()signal()signalAll。并且,调用 Condition 对象的 signal()signalAll() 是唤醒被它自身挂起的任务。

也就是说,对于不同的 Condition 对象,谁挂起的任务,谁唤醒。

回到我们的例子中,有两组任务:发布 apk 的任务和测试 apk 的任务。这两组任务需要经历挂起,唤醒的操作。那么,我们自然需要两个 Condition 对象。

private Condition releaseCondition = lock.newCondition();
private Condition testCondition = lock.newCondition();

releaseCondition 负责发布 apk 这个任务的挂起和唤醒;
testCondition 负责测试 apk 这个任务的挂起和唤醒。

public void releaseApk(String name) {
    lock.lock();
    try {
        while (isForTest) {
            try {
            	// 挂起发布 apk 的任务
                releaseCondition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            // 这 200 ms 当作开发时间
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.apkName = name +"-V"+ code;
        System.out.println(Thread.currentThread().getName() + "============>Release apk: " + this.
        code++;
        isForTest = true;
        // 唤醒测试 apk 的任务
        testCondition.signal();
    } finally {
        lock.unlock();
    }
}
public void testApk() {
    lock.lock();
    try {
        while (!isForTest) {
            try {
            	// 挂起测试 apk 的任务
                testCondition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            // 这 60 ms 作为测试时间
            TimeUnit.MILLISECONDS.sleep(60);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " Test Apk: " + this.apkName);
        isForTest = false;
        // 唤醒发布 apk 的任务
        releaseCondition.signal();
    } finally {
        lock.unlock();
    }
}

这样就实现了指定唤醒的目标。

7.4 多开发多测试的实例

目前,多开发多测试的例子还是与实际不相符:例子中两个开发工程师,两个测试工程师,却是一个 apk 发布,一个 apk 测试这样的节奏在工作。这不是人浮于事吗?

实际中,会有一个待测 apk 的集合,比如这个集合的大小是 5,
开发工程师在判断集合不满时,说明集合中还可以存放新的 apk,就发布 apk;满时就不发布 apk。
测试工程师在判断集合不空时,说明有 apk 待测,就从集合中取出一个,开始测试;为空时,就不测试 apk.。

实现的代码如下:

class Apk {
    private String apkName;
    private static int counter = 1;
    private final int code = counter++;
    public Apk(String apkName) {
        this.apkName = apkName;
    }

    @Override
    public String toString() {
        return "Apk: " + apkName + "-V" + code;
    }
}

class ApkBuffer {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    final Apk[] items = new Apk[5];
    private int putptr, takeptr, count;

    public void put(Apk x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            System.out.println("ApkBuffer, put=======>" + x);
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Apk take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Apk x = items[takeptr];
            System.out.println("ApkBuffer: take<===================" + x);
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }

}
class ReleaseApkRunnable implements Runnable {
    private ApkBuffer apkBuffer;
    public ReleaseApkRunnable(ApkBuffer apkBuffer) {
        this.apkBuffer = apkBuffer;
    }

    @Override
    public void run() {
        while (true) {
            try {
                apkBuffer.put(new Apk("QQ"));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class TestApkRunnable implements Runnable {
    private ApkBuffer apkBuffer;

    public TestApkRunnable(ApkBuffer apkBuffer) {
        this.apkBuffer = apkBuffer;
    }

    @Override
    public void run() {
        while (true) {
            try {
                apkBuffer.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ApkDemo {
    public static void main(String[] args) {
        Apk apk = new Apk("QQ");
        ApkBuffer apkBuffer = new ApkBuffer();
        Runnable releaseApkRunnable = new ReleaseApkRunnable(apkBuffer);
        Runnable testApkRunnable = new TestApkRunnable(apkBuffer);
        Thread releaseThread1 = new Thread(releaseApkRunnable, "releaseThread1");
        Thread releaseThread2 = new Thread(releaseApkRunnable, "releaseThread2");
        Thread testThread1 = new Thread(testApkRunnable, "testThread1");
        Thread testThread2 = new Thread(testApkRunnable, "testThread2");
        releaseThread1.start();
        releaseThread2.start();
        testThread1.start();
        testThread2.start();
    }
}

这段代码不再详细说明了。因为核心代码和之前的例子是一样的。
需要说明的是:
Apk 类中:

 private static int counter = 1;
 private final int code = counter++;

这是为了保证 apk 的 code 值按次序递增。

8 线程的终止

Thread 类的 stop() 方法 :这个方法已过时,具有不安全性;
Thread 类的 suspend() 方法 :这个方法已过时,具有死锁倾向。
使得 run() 方法结束:run() 方法里有循环结构时,判断循环标记不满足时,就结束循环。这种方式在某些情况下不可靠。
我们通过一个小例子来说明:

class MyRunnable implements Runnable {
    private FlagBean flagBean;

    public MyRunnable(FlagBean flagBean) {
        this.flagBean = flagBean;
    }

    @Override
    public synchronized void run() {
        while (!flagBean.isFlag()) {
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "...." + e);
            }
        }
    }
}
class FlagBean {
    private boolean flag;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
public class UseFlag {
    public static void main(String[] args) {
        FlagBean flagBean = new FlagBean();
        MyRunnable target = new MyRunnable(flagBean);
        Thread thread1 = new Thread(target);
        Thread thread2 = new Thread(target);
        thread1.start();
        thread2.start();
        int i = 0;
        while (true) {
            if (i >= 30) {
                flagBean.setFlag(true);
                break;
            }
            System.out.println(Thread.currentThread().getName() + "。。。" + (i++));
        }
    }
}

flagBeanflag 标记为 true 时,就结束循环,完成 run() 方法的执行,线程也就该自然结束了。但是,在此之前,while 循环里,两个线程都执行到了 wait() 方法,它们都被挂起了,处于阻塞状态。即便是改变了标记,因为没有执行唤醒的操作,也没有机会再去判断标记,进而结束循环。

需要注意的是我们在 run 方法上加上了 synchronized 关键字,这是一个同步方法。这是因为我们在 run() 方法里使用了 wait() 方法,这需要当前线程持有锁对象。否则,会抛出 IllegalMonitorStateException

通过使用 Threadinterrupte() 方法来使等待中的任务结束。把线程从阻塞状态中断变为就绪状态。
例子如下:

class MyRunnable implements Runnable {
    private FlagBean flagBean;

    public MyRunnable(FlagBean flagBean) {
        this.flagBean = flagBean;
    }

    @Override
    public synchronized void run() {
        while (!flagBean.isFlag()) {
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "...." + e);
                flagBean.setFlag(true);
            }
        }
    }
}

class FlagBean {
    private boolean flag;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

public class UseInterrupt {
    public static void main(String[] args) {
        FlagBean flagBean = new FlagBean();
        MyRunnable target = new MyRunnable(flagBean);
        Thread thread1 = new Thread(target);
        Thread thread2 = new Thread(target);
        thread1.start();
        thread2.start();
        int i = 0;
        while (true) {
            if (i >= 30) {
                thread1.interrupt();
                thread2.interrupt();
                break;
            }
            System.out.println(Thread.currentThread().getName() + "。。。" + (i++));
        }
    }
}

9 需要区分的概念

sleep() 方法和 wait() 方法的区别

  • 所属不同:sleep() 方法在 Thread 类中,wait() 方法在 Object 类中;
  • 参数不同:sleep() 方法必须指定时间,wait() 方法可以指定时间也可以不指定时间;
  • 在同步中,对 CPU 的执行权和锁的处理不同:wait() 方法释放 CPU 执行权,并且释放锁;sleep() 方法释放 CPU 执行权,不释放锁。
  • Thread.yield() 被调用后,持有锁的线程不会释放锁。

Thread.interrupted()isInterrupted() 方法的区别

public static boolean interrupted() {
   return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
  	return isInterrupted(false);
}

Thread.interrupted() 方法会清除 ClearInterruptedtrueisInterrupted() 方法不会清除 ClearInterrupted

公平锁与非公平锁
synchronized 内置锁默认是非公平锁,不可以更改;ReentrantLock 默认是非公平锁,可以通过参数设置为公平锁。

可重入锁
synchronized 内置锁是可重入锁,也就是说,一个 synchronized 修饰的方法 g(),获取到锁之后,进入方法体内,方法体内又去调用 g(),仍能够获取到锁,而不会造成死锁。

ReentrantLock 是可重入锁。

排他锁
synchronized 内置锁是排他锁,ReentrantLock 是排他锁,ReadWriteLock 不是排他锁。
排他锁就是在同一时刻只能允许一个线程访问。

文章涉及的代码在https://github.com/jhwsx/Java_01_AdvancedFeatures/tree/master/src/com/java/advanced/features/concurrent

参考

发布了84 篇原创文章 · 获赞 56 · 访问量 8万+
App 阅读领勋章
微信扫码 下载APP
阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览