前置知识

  • 希望你不是一个初学者
  • 线程安全问题,需要你接触过JavaWeb开发、JDBC开发、Web服务器、分布式框架时才会遇到
  • 采用slf4j打印日志
  • 采用lombok简化JavaBean的书写
  • 基于JDK8,最好对函数式编程、lambda有一定了解,这部分知识在我的站内也有对应的笔记
  • 创建一个Maven项目,所需的pom依赖如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
  • logback.xml的配置如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<configuration
xmlns="http://ch.qos.logback/xml/ns/logback"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{HH:mm:ss.SSS} %c [%t] - %m%n</pattern>
</encoder>
</appender>
<logger name="c" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
<root level="ERROR">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

进程与线程

  • 本节内容
    1. 进程和线程的概念
    2. 并行和并发的概念
    3. 线程基本应用

进程与线程

  • 进程
    • 程序由指令和数据组成,但这些指令要运行,数据要读写,那么就需要将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的
    • 当一个程序被运行,从磁盘加载这个程序的代码至内存,此时就开启了一个进程
    • 进程可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网抑云、Steam、Eipc等)
  • 线程
    • 一个进程之内可以分一道多个线程
    • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
    • Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在Windows中的进程是不活动的,只是作为线程的容器
  • 二者对比
    • 进程基本上是相互独立的,而线程存在于进程内,是进程的一个子集
    • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
    • 进程间通信较为复杂
      • 同一台计算机的进程通信称为IPC(Inter-process Communication)
      • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
    • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
    • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

并行与并发

  • 单核CPU下,线程实际上是串行执行的。操作系统中有一个组件叫做任务调度器,将CPU的时间片分给不同的程序使用,只是由于CPU在线程间的切换非常快(因为时间片很小),所以给我们的感觉是同时运行的。总结为一句话就是:微观串行,宏观并行。
  • 一般会将这种线程轮流使用CPU的做法称为并发(Concurrent)
CPU 时间片1 时间片2 时间片3 时间片4
core 线程1 线程2 线程3 线程4

  • 多核CPU下,每个核(core)都可以调度运行线程,这时候线程可以是并行的。
CPU 时间片 1 时间片 2 时间片 3 时间片 4
core 1 线程 1 线程 3 线程 1 线程 3
core 2 线程 2 线程 4 线程 2 线程 4

  • 小结
    • 并发(Concurrent):是同一时间应对(dealing with)多件事情的能力
    • 并行(Parallel):是同一时间动手做(doing)多件事情的能力
  • 例子
    • 最近刚好把分手厨房通关了,就拿它举例吧:游戏需要切菜、烹饪、装盘、送菜等多个步骤,如果现在是单人轮流交替做这多件事,此时就是并发
    • 联机叫了三个朋友一起打,一个只负责切菜、一个只负责烹饪、一个只负责装盘、一个只负责送菜,互不干扰,此时就是并行
    • 但是大多数情况下凑不齐那么多人,如果两个人一起做这些事,此时既有并发,也有并行,此时会产生竞争,例如锅只有一口,一个人用锅的时候,另一个人就得等待

应用

异步调用

  • 以调用方角度来讲,如果
    • 需要等待结果返回,才能继续运行,这是同步
    • 不需要等待结果返回,就能继续运行,这是异步
  • 设计
    • 多线程可以让方法执行变为异步,例如读取磁盘文件时,假设此次读取耗时5秒,如果没有线程调度机制,这5秒CPU什么也做不了,其他代码都得暂停
  • 结论
    • 例如在项目中,视频文件的格式转换等操作比较耗时,此时开一个新的线程去处理视频转换,可以避免阻塞主线程
    • Tomcat的异步servlet也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞Tomcat的工作线程

提高效率

  • 充分利用多核CPU的优势,提高运行效率,想象下面的场景,执行三个计算,最后将计算结果汇总
1
2
3
4
5
6
7
计算 1 花费 10 ms

计算 2 花费 12 ms

计算 3 花费 8 ms

汇总 花费 2 ms
  • 此时如果是串行执行,那么需要花费10 + 12 + 8 + 2 = 32ms

  • 但如果是4核CPU,各个核心分别使用线程1执行计算1、线程2执行计算2、线程3执行计算3,那么3个线程是并行的,花费时间只取决于最长的那个线程的运行时间,即12ms,再加上汇总时间所需的2ms,总时长仅仅才14ms

  • 在单核CPU下,多线程下不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用CPU,不至于一个线程总占用CPU,导致别的线程没法干活

  • 多核CPU可以并行跑多个线程,但能否提高程序运行效率还是要分情况的

    • 有些任务经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但并不是所有计算任务都能拆分(参考后文的阿姆达尔定律
    • 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没有意义
  • IO操作不占用CPU,只是我们一般拷贝文件使用的是阻塞式IO,这相当于线程虽然没用CPU,但需要一直等待IO结束,没能充分利用线程。所以才有后面的非阻塞式IO异步IO的优化

Java线程

  • 本节内容
    1. 创建和运行线程
    2. 查看线程
    3. 线程API
    4. 线程状态

创建和运行线程

方式一:直接使用Thread

  • 示例代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws IOException {
// 匿名内部类创建线程,起名为t1
Thread thread = new Thread("t1") {
@Override
public void run() {
log.debug("running");
}
};
// 启动线程
thread.start();
}
}
  • 输出
1
14:23:39 [t1] c.Sync - running

方式二:Runnable配合Thread

  • 把创建线程和执行任务的代码分开,Thread创建线程,Runnable执行任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws IOException {
// 创建任务
Runnable runnable = new Runnable() {
@Override
public void run() {
log.debug("running");
}
};
// 创建线程,指定任务,起名为t1
Thread thread = new Thread(runnable, "t1");
// 启动线程
thread.start();
}
}
  • 输出
1
14:26:18 [t1] c.Sync - running
  • Java8以后还可以用lambda简化代码
1
2
3
4
5
6
7
8
9
10
11
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws IOException {
// 创建任务,lambda简化代码
Runnable runnable = () -> log.debug("running");
// 创建线程,指定任务,起名为t1
Thread thread = new Thread(runnable, "t1");
// 启动线程
thread.start();
}
}
  • 当然还可以进一步精简
1
2
3
4
5
6
7
8
9
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) throws IOException {
// 创建线程,指定任务,起名为t1
Thread thread = new Thread(() -> log.debug("running"), "t1");
// 启动线程
thread.start();
}
}

原理之Thread与Runnable的关系

  • 我们来分析一下Thread的源码,理清它与Runnable的关系,下面的代码我只截取了我们上面用到的部分
1
2
3
4
5
6
7
8
9
10
11
12
13
/* What will be run. */
private Runnable target;

public Thread(Runnable target, String name) {
init(null, target, name, 0);
}

@Override
public void run() {
if (target != null) {
target.run();
}
}
  • 从Thread的源码中,我们可以发现,我们用方式二创建线程时,传入了一个Runnable对象,该对象作为Thread的一个名为target的属性,当我们调用run方法时,如果target不为空,那么调用的是target的run()方法
  • 但是当我们使用方式一的时候,没有传入Runnable对象,此时是重写了Thread的run()方法
  • 小结
    • 方式一是把线程和任务合并在了一起,方法二是把线程和任务分开了
    • 使用Runnable对象,更容易与线程池等高级API配合
    • 使用Runnable对象让任务类脱离了Thread的继承体系,更灵活

方式三:FutureTask配合Thread

  • FutureTask能够接受Callable类型的参数,用来处理有返回结果的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建任务对象
FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running");
return 521;
}
});
// 第一个参数是任务对象,第二个参数是线程名称
Thread thread = new Thread(task, "t1");
// 启动线程
thread.start();
// 使用task.get()会阻塞线程直至获取到结果,这里日志打印
log.debug("获取到返回值:{}", task.get());
  • 上述代码也可以用lambda简化
1
2
3
4
5
6
7
FutureTask<Integer> task = new FutureTask<>(() -> {
log.debug("running");
return 521;
});
Thread thread = new Thread(task, "t1");
thread.start();
log.debug("获取到返回值:{}", task.get());
  • 输出
1
2
14:58:06 [t1] c.Sync - running
14:58:06 [main] c.Sync - 获取到返回值:521
  • 源码剖析
    • FutureTask实现了RunnableFuture接口
    1
    public class FutureTask<V> implements RunnableFuture<V> 
    • 而RunnableFuture又实现了Runnable接口和Future接口
    1
    2
    3
    4
    5
    6
    7
    public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
    * Sets this Future to the result of its computation
    * unless it has been cancelled.
    */
    void run();
    }
    • Future接口中有一个方法可以返回任务执行的结果,泛型V即返回值类型
    1
    V get() throws InterruptedException, ExecutionException;
    • Callable接口有返回值,并且可以抛出异常
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @FunctionalInterface
    public interface Callable<V> {
    /**
    * Computes a result, or throws an exception if unable to do so.
    *
    * @return computed result
    * @throws Exception if unable to compute a result
    */
    V call() throws Exception;
    }

观察多个线程同时运行

  • 两个线程交替执行,谁先谁后不受我们控制
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
new Thread(() -> {
while (true)
log.debug("running");
}, "t1").start();
new Thread(() -> {
while (true)
log.debug("running");
}, "t2").start();
}
  • 输出
1
2
3
4
5
6
7
8
9
10
15:05:16 [t1] c.Sync - running
15:05:16 [t2] c.Sync - running
15:05:16 [t2] c.Sync - running
15:05:16 [t2] c.Sync - running
15:05:16 [t2] c.Sync - running
15:05:16 [t2] c.Sync - running
15:05:16 [t2] c.Sync - running
15:05:16 [t1] c.Sync - running
15:05:16 [t1] c.Sync - running
15:05:16 [t2] c.Sync - running

查看进程线程的方法

Windows

  • 在Windows环境下,可以通过任务管理器来查看进程和线程数,也可以用来杀死进程
    • tasklist 查看进程
    • taskkill 杀死进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
C:\WINDOWS\system32>tasklist | findstr java
java.exe 20900 Console 1 63,024 K
java.exe 17244 Console 1 26,252 K

C:\WINDOWS\system32>jps
15780 Jps
20900 RemoteMavenServer36
18760
17244 Launcher

C:\WINDOWS\system32>jps
20896 Launcher
14788 Sync
20900 RemoteMavenServer36
6372 Jps
18760

C:\WINDOWS\system32>taskkill /F /PID 14788
成功: 已终止 PID 为 14788 的进程。

Linux

  • Linux环境下有关进程的指令
    • ps -ef 查看所有进程
    • ps -fT -p <PID> 查看某个进程(PID)的所有线程
    • kill 杀死进程
    • top 按大写H切换是否显示进程
    • top -H -p <PID> 查看某个进程(PID)的所有线程

Java

  • jps命令查看所有Java进程
  • jstack 查看某个Java进程(PID)的所有线程状态
  • jconsole 查看某个Java进程中线程的运行情况(图形界面)

原理之线程运行

栈与栈帧

  • Java虚拟机栈(Java Virtual Machine Stacks)是Java虚拟机为每个线程分配的一块内存区域,用于存储线程的方法调用和局部变量等信息。
  • 每个线程在运行时都有自己的Java虚拟机栈,线程开始时会创建一个新的栈帧(Stack Frame),用于存储该线程的方法调用信息。当方法调用完成后,该栈帧会被弹出,回到上一次方法调用的位置。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
  • 每个线程都有自己的栈帧,栈帧包含方法的参数、局部变量和返回值等信息,因此不同的线程可以在不相互干扰的情况下同时访问相同的方法。下面我们来编写测试代码验证一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
new Thread(() -> {
method1();
}, "t1").start();
method1();
}

static void method1() {
log.debug("here's method1");
method2();
}

static void method2() {
log.debug("here's method2");
}
  • 打断点注意选择挂起线程
  • 对main线程,依次步入method2()方法,不会影响到t1线程的操作;同样,对t1线程,依次步入到method2()方法,也不会影响main线程。证实了每个线程都有自己的栈帧,且可以在不互相干扰的环境下同时访问相同的方法。
  • 关于虚拟机栈的更详细的内容在我这篇文章的第二小节中有提及

线程上下文切换

  • 线程上下文切换(Thread Context Switch):因为某些原因导致不再执行当前线程,转而执行另一个线程的代码
    • 线程的CPU时间片用完
    • 垃圾回收
    • 有更高优先级的线程需要运行
    • 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法

常见方法

方法名 static 功能说明 注意
start() 启动一个新线程,在新的线程运行run方法中的代码 start方法只是让线程进入就绪,里面代码不一定立刻运行(CPU的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run() 新线程启动后会调用的方法 如果在构造Thread对象时传递了Runnable参数,则线程启动后会调用Runnable中的run方法,否则默认不执行任何操作。但可以创建Thread的子类对象,来覆盖默认行为
join() 等待线程运行结束
join(long n) 等待线程运行结束,最多等待n毫秒
getId() 获取线程长整型的id id唯一
getName() 获取线程名
setName(String) 修改线程名
getPriority() 获取线程优先级
setPriority(int) 修改线程优先级 Java中规定线程优先级是1~10的整数,较大的优先级能提高该线程被CPU调度的机率
getState() 获取线程状态 Java中线程状态是用6个enum表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted() static 判断是否被打断 不会清除打断标记
isAlive() static 线程是否存活(还没有运行完毕)
interrupt() static 打断线程 如果被打断线程正在sleep、wait、join会导致被打断的线程抛出InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park的线程被打断,也会设置打断标记
interrupted() static 判断当前线程是否被打断 会清除打断标记

start与run

调用run

  • 示例代码如下
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
@Slf4j(topic = "c.Sync")
public class Sync {
static final String MP4_FULL_PATH = "D:\\BaiduNetdiskDownload\\CowboyBebop.mp4";
public static void main(String[] args) {
new Thread(() -> {
log.debug(Thread.currentThread().getName());
try {
read(MP4_FULL_PATH);
} catch (Exception e) {
throw new RuntimeException(e);
}
}, "t1").run();
log.debug("do other things ...");
}

public static void read(String fileName) throws Exception {
FileInputStream fis = new FileInputStream(fileName);
byte[] buffer = new byte[1024];
long start = System.currentTimeMillis();
int len;
while ((len = fis.read(buffer)) != -1) {
}
long end = System.currentTimeMillis();
log.debug("读取文件耗时:{}ms", end - start);
}
}

  • 输出
1
2
3
17:42:59 [main] c.Sync - main
17:43:03 [main] c.Sync - 读取文件耗时:3925ms
17:43:03 [main] c.Sync - do other things ...
  • 程序仍在man线程运行,read()方法调用还是同步的

调用start

  • 将上述代码中的run()改为start(),输出如下,此时读取文件是在t1线程下完成的
1
2
3
17:50:13 [main] c.Sync - do other things ...
17:50:13 [t1] c.Sync - t1
17:50:17 [t1] c.Sync - 读取文件耗时:3844ms

小结

  • 直接调用run()是在主线程中执行了run(),并没有直接启动新线程
  • 使用start是启动新的线程,通过新的线程间接执行run()中的代码

sleep和yield

sleep

  1. 调用sleep会让当前线程从Running进入Timed Waiting状态(阻塞)

    • 测试代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }, "t1");

    t1.start();
    log.debug("t1状态:{}",t1.getState());
    Thread.sleep(1000);
    log.debug("t1状态:{}",t1.getState());
    }
    • 结果如下,由于主线程先执行,所以此时t1状态为RUNNABLE,主线程休眠1s后,再次查看t1状态,此时t1是阻塞状态
    1
    2
    12:03:23.500 [main] c.Sync - t1状态:RUNNABLE
    12:03:24.500 [main] c.Sync - t1状态:TIMED_WAITING
  2. 其他线程可以使用interrupt方法打断正在睡眠的线程,此时sleep方法会抛出InterruptedException

    • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    try {
    log.debug("t1进入睡眠");
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    log.debug("t1被打断醒来");
    throw new RuntimeException(e);
    }
    }, "t1");

    t1.start();
    log.debug("准备打断t1");
    Thread.sleep(500);
    t1.interrupt();

    }
    • 结果如下
    1
    2
    3
    4
    12:09:12.789 [t1] c.Sync - t1进入睡眠
    12:09:12.789 [main] c.Sync - 准备打断t1
    12:09:13.842 [t1] c.Sync - t1被打断醒来
    Exception in thread "t1" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
  3. 睡眠结束后的线程未必会立刻得到执行,因为此时CPU可能在执行其他的线程,当当前线程获取到时间片时才会得到执行

  4. 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性

    • TimeUnit是一个枚举类,提供了对时间单位的抽象表示,如秒、毫秒、微秒等。它的sleep方法接受一个时间值和一个TimeUnit参数,用于指定线程休眠的时间。例如,TimeUnit.SECONDS.sleep(1)表示线程休眠1秒,同时我们也可以将SECONDS换成HOUR、DAYS等时间值

yield

  1. 当一个线程执行到 yield 语句时,它会暂停当前的执行,将CPU的执行权交给其他线程。
    • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    int count = 0;
    while (true) {
    System.out.println("t1 --> " + count++);
    }
    }, "t1");
    Thread t2 = new Thread(() -> {
    int count = 0;
    while (true) {
    // 执行yield
    Thread.yield();
    System.out.println(" t2 --> " + count++);
    }
    }, "t2");
    t1.start();
    t2.start();
    }
    • 结果如下,t1输出了10w条,t2才1.7w
    1
    2
    3
    4
    5
    t1 --> 106221
    t1 --> 106222
    t2 --> 17256
    t1 --> 106223
    t1 --> 106224
  2. 具体的实现依赖于操作系统的任务调度器

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没作用
    • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    int count = 0;
    while (true) {
    System.out.println("t1 --> " + count++);
    }
    }, "t1");
    Thread t2 = new Thread(() -> {
    int count = 0;
    while (true) {
    System.out.println(" t2 --> " + count++);
    }
    }, "t2");
    // t1设置最低优先级
    t1.setPriority(Thread.MIN_PRIORITY);
    // t2设置最高优先级
    t2.setPriority(Thread.MAX_PRIORITY);
    t1.start();
    t2.start();
    }
    • 结果如下,t1打印的比t2多,CPU闲的时候,优先级几乎没作用 (它已经是个成熟的调度器了,有自己的想法)
    1
    2
    3
    4
            t2 --> 64136
    t2 --> 64137
    t1 --> 90915
    t1 --> 90916

join方法详解

为什么需要join

  • 我们用下面的代码作为引子,打印的r是什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int r = 0;
public static void main(String[] args) throws InterruptedException {
log.debug("主线程开始");
Thread t1 = new Thread(() -> {
log.debug("t1开始");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
r = 10;
log.debug("t1结束");
}, "t1");
t1.start();
log.debug("结果为:{}", r);
log.debug("主线程结束");
}
  • 分析
    • 因为主线程和t1是并行执行的,t1线程需要休眠1s后才能执行r = 10
    • 而主线程一开始就要打印r的结果,所以只能输出r = 0
    1
    2
    3
    4
    5
    13:09:22.864 [main] c.Sync - 主线程开始
    13:09:22.958 [t1] c.Sync - t1开始
    13:09:22.123 [main] c.Sync - 结果为:0
    13:09:22.123 [main] c.Sync - 主线程结束
    13:09:23.234 [t1] c.Sync - t1结束
  • 解决方法
    • 在t1.start()后面加上t1.join(),这样主线程会等待t1执行完毕,最终r = 10
    1
    2
    3
    4
    5
    13:12:25.864 c.Sync [main] - 主线程开始
    13:12:25.915 c.Sync [t1] - t1开始
    13:12:26.929 c.Sync [t1] - t1结束
    13:12:26.929 c.Sync [main] - 结果为:10
    13:12:26.931 c.Sync [main] - 主线程结束

等待多个结果

  • 下面代码耗时大约多久?
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 static void main(String[] args) throws InterruptedException {
// 休眠1s
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
r1 = 10;
});
// 休眠2s
Thread t2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1:{},r2:{},耗时:{}ms", r1, r2, end - start);
}
  • 分析一下
    • t1.join(),等待t1执行完毕,但t2也没有停止,也在运行
    • t2.join(),1s后,执行到此,t2也运行了1s,再等待1s也执行完毕了
  • 如果颠倒两个join,运行结果也不会变,最终耗时均为2s左右
1
13:27:50.041 c.Sync [main] - r1:10,r2:20,耗时:2010ms

有时效的join

  • join()方法还有一个带参数的,可以设定最大等待时长毫秒数
    • 没等够,输出:14:09:44.417 c.Sync [main] - r:0,耗时:511ms
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    static int r = 0;

    public static void main(String[] args) throws InterruptedException {
    // 休眠1s
    Thread t1 = new Thread(() -> {
    try {
    TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    r = 10;
    });
    long start = System.currentTimeMillis();
    t1.start();
    // 最多等0.5s
    t1.join(500);
    long end = System.currentTimeMillis();
    log.debug("r:{},耗时:{}ms", r, end - start);
    }
    • 等够了,线程执行结束会导致join结束,虽然等1.5s,但1s就结束,耗时1s:14:10:47.975 c.Sync [main] - r:10,耗时:1001ms
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    static int r = 0;

    public static void main(String[] args) throws InterruptedException {
    // 休眠1s
    Thread t1 = new Thread(() -> {
    try {
    TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    r = 10;
    });
    long start = System.currentTimeMillis();
    t1.start();
    // 最多等1.5s
    t1.join(1500);
    long end = System.currentTimeMillis();
    log.debug("r:{},耗时:{}ms", r, end - start);
    }

interrupt方法详解

打断sleep、wait、join的线程

  • 这几个方法都会让线程进入阻塞状态,打断slee的线程,会清空打断状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws InterruptedException {
// 休眠2s
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});

t1.start();
// 休眠1s,打断t1
TimeUnit.SECONDS.sleep(1);
t1.interrupt();
log.debug("t1打断状态:{}", t1.isInterrupted());
}
  • 结果
1
2
3
4
5
6
7
8
9
10
Exception in thread "Thread-0" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
at com.cyborg2077.demo01.Sync.lambda$main$0(Sync.java:25)
at java.lang.Thread.run(Thread.java:750)
Caused by: java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:342)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.cyborg2077.demo01.Sync.lambda$main$0(Sync.java:23)
... 1 more
14:33:07.789 c.Sync [main] - t1打断状态:false

打断正常运行的线程

  • 打断正常运行的线程,不会清空打断状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
Thread thread = Thread.currentThread();
boolean interrupted = thread.isInterrupted();
if (interrupted) {
log.debug("打断状态:{}", interrupted);
break;
}
}
});

t1.start();
// 休眠1s,打断t1
TimeUnit.SECONDS.sleep(1);
t1.interrupt();
}
  • 结果
1
14:38:54.278 c.Sync [t2] -  打断状态: true

打断park线程

  • 打断park线程,不会清空打断状态
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
  • 输出
1
2
3
16:33:57.325 c.Sync [t1] - park...
16:33:58.329 c.Sync [t1] - unpark...
16:33:58.329 c.Sync [t1] - 打断状态:true
  • 如果打断标记已经是true,则park会失效,会一直打印这两条日志
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true){
log.debug("park...");
LockSupport.park();
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
  • 输出
c.Sync [t1] - park...
1
2
3
4
5
6
7
8
16:34:51.588 c.Sync [t1] - 打断状态:true
16:34:51.588 c.Sync [t1] - park...
16:34:51.588 c.Sync [t1] - 打断状态:true
16:34:51.588 c.Sync [t1] - park...
16:34:51.588 c.Sync [t1] - 打断状态:true
16:34:51.588 c.Sync [t1] - park...
16:34:51.588 c.Sync [t1] - 打断状态:true
16:34:51.588 c.Sync [t1] - park...
  • 我们可以使用Tread.interrupted()清除打断状态
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.interrupted());
LockSupport.park();
log.debug("unpark...");
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
  • 输出
1
2
3
16:37:37.979 c.Sync [t1] - park...
16:37:38.982 c.Sync [t1] - unpark...
16:37:38.982 c.Sync [t1] - 打断状态:true

不推荐使用的方法

  • 这三个方法已经过时,而且容易破坏同步代码块,造成线程死锁
方法名 static 功能说明
stop() 停止线程运行
suspend() 挂起(暂停)线程运行
resume() 恢复线程运行

终止模式之两阶段终止模式

  • 两阶段终止模式(Two Phase Termination)
    • 在一个线程T1中如何优雅的终止线程T2?这里的优雅指的是给T2一个料理后事的机会

错误思路

  • 使用线程对象的stop()方法停止线程
    • stop方法会真正杀死线程,如果此时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁
  • 使用System.exit()方法停止线程
    • 目的仅仅是停止一个线程,但是这种方法会让整个程序都停止

两阶段终止模式-interrupt

  • 利用isInterrupted
    • interrupt可以打断正在执行的线程,无论这个线程是在sleep、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
    @Slf4j(topic = "c.TPTInterrupt")
    class TPTInterrupt {
    private Thread thread;

    public static void main(String[] args) throws InterruptedException {
    TPTInterrupt t = new TPTInterrupt();
    t.start();
    Thread.sleep(5000);
    t.stop();
    }

    public void start() {
    thread = new Thread(() -> {
    while (true) {
    Thread currentThread = Thread.currentThread();
    boolean interrupted = currentThread.isInterrupted();
    if (interrupted) {
    log.debug("料理后事");
    break;
    }
    try {
    Thread.sleep(2000);
    log.debug("执行监控记录");
    } catch (InterruptedException e) {
    e.printStackTrace();
    // 因为sleep被打断后会清除打断标记为false,所以这里重新设置打断标记
    thread.interrupt();
    }
    }
    }, "监控线程");
    thread.start();
    }

    public void stop() {
    thread.interrupt();
    }
    }
    • 结果
    1
    2
    3
    4
    5
    6
    7
    15:47:09.479 c.TPTInterrupt [监控线程] - 执行监控记录
    15:47:11.484 c.TPTInterrupt [监控线程] - 执行监控记录
    java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at com.cyborg2077.demo01.TPTInterrupt.lambda$start$0(Sync.java:59)
    at java.lang.Thread.run(Thread.java:750)
    15:47:12.472 c.TPTInterrupt [监控线程] - 料理后事

主线程与守护线程

  • 默认情况下,Java线程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束
  • 示例代码如下,将t1线程设置为守护线程,主线程即为非守护线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("守护线程开始执行");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("守护线程执行结束");
}, "t1");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.debug("非守护线程执行结束");
}
  • 输出如下,当主线程执行结束,守护线程未打印守护线程执行结束,就被强制结束了
1
2
18:23:07.989 c.Sync [t1] - 守护线程开始执行
18:23:08.989 c.Sync [main] - 非守护线程执行结束
  • 注意:
    • 垃圾回收线程就是一种守护线程
    • Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后秒回等待它们处理完当前请求

五种状态

  • 五种状态是从操作系统层面来描述的
    • 初始状态仅是在语言层面创建了线程对象,还未与操作系统线程相关联
    • 可运行状态(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由CPU调度执行
    • 运行状态指获取了CPU时间片运行中的状态
      • 当CPU时间片用完,会从运行状态转换至可运行状态,会导致线程上下文的切换
    • 阻塞状态
      • 如果调用了阻塞API,如BIO读写文件,此时该线程实际不会用到CPU,会导致上下文切换,进入阻塞状态
      • 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态
      • 与可运行状态的区别是,对阻塞状态的线程来说,只要它们一直不唤醒,调度器就一直不会考虑调度它们
    • 终止状态表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态

六种状态

  • 这是从Java API层面来描述的,根据Thread State枚举,分为六种状态
    • NEW:线程刚被创建,但还没有调用start()方法
    • RUNNABLE:当调用了start()方法后

      注意:Java API层面的RUNNABLE状态涵盖了操作系统层面的可运行状态、运行状态和阻塞状态(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行)

    • BLOCKEDWAITINTTIMED_WAITING都是Java API层面对阻塞状态的细分,后面会在状态转换一节详细描述
    • TERMINATED:当线程代码运行结束

  • 下面我们用代码来验证一下这六种状态
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
@Slf4j(topic = "c.TestState")
public class TestState {
public static void main(String[] args) throws IOException {
// 1. t1线程刚被创建,还未调用start()方法,此时状态为 NEW
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};

// 2. t2线程已被创建,并且调用了start()方法,空循坏会保证t2线程不会结束,此时状态为RUNNABLE
Thread t2 = new Thread("t2") {
@Override
public void run() {
while(true) { // runnable

}
}
};
t2.start();

// 3. t3线程只是打印一条日志,由于主线程中调用了sleep(),故当我们查看t3线程状态时,t3已经执行完毕,状态为TERMINATED
Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running...");
}
};
t3.start();

// 4. t4线程中调用了sleep()方法,状态为timed_waiting,即有时限的等待,注意此时t4还拿到了一把锁,后面要用
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (TestState.class) {
try {
Thread.sleep(1000000); // timed_waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();

// 5. t5线程中调用了t2.join(),需要等待t2线程结束,故状态为waiting,即无时限的等待
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join(); // waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();

// 6. 由于t4线程拿到了锁,但由于t4在sleep,故t6线程拿不到锁,会被阻塞,状态为blocked
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (TestState.class) { // blocked
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();

try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
System.in.read();
}
}
  • 输出
1
2
3
4
5
6
7
11:02:48.753 c.TestState [t3] - running...
11:02:49.264 c.TestState [main] - t1 state NEW
11:02:49.265 c.TestState [main] - t2 state RUNNABLE
11:02:49.265 c.TestState [main] - t3 state TERMINATED
11:02:49.265 c.TestState [main] - t4 state TIMED_WAITING
11:02:49.265 c.TestState [main] - t5 state WAITING
11:02:49.265 c.TestState [main] - t6 state BLOCKED

应用:烧水泡茶

  • 阅读华罗庚《统筹方法》,给出烧水泡茶的多线程解决方案
  • 统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复杂的科研项目的组织与管理中,都可以应用。

  • 怎样应用呢?主要是把工序安排好。

  • 比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么办?

    • 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开了,泡茶喝。
    • 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡茶喝。
    • 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。
  • 哪一种办法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。

  • 这是小事,但这是引子,可以引出生产管理等方面有用的方法来。

  • 水壶不洗,不能烧开水,因而洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯,就不能泡茶,因而这些又是泡茶的前提。它们的相互关系,可以用下边的箭头图来表示:

  • 从这个图上可以一眼看出,办法甲总共要16分钟(而办法乙、丙需要20分钟)。如果要缩短工时、提高工作效率,应当主要抓烧开水这个环节,而不是抓拿茶叶等环节。同时,洗茶壶茶杯、拿茶叶总共不过4分钟,大可利用“等水开”的时间来做。

  • 是的,这好像是废话,卑之无甚高论。有如走路要用两条腿走,吃饭要一口一口吃,这些道理谁都懂得。但稍有变化,临事而迷的情况,常常是存在的。在近代工业的错综复杂的工艺过程中,往往就不是像泡茶喝这么简单了。任务多了,几百几千,甚至有好几万个任务。关系多了,错综复杂,千头万绪,往往出现“万事俱备,只欠东风”的情况。由于一两个零件没完成,耽误了一台复杂机器的出厂时间。或往往因为抓的不是关键,连夜三班,急急忙忙,完成这一环节之后,还得等待旁的环节才能装配。洗茶壶,洗茶杯,拿茶叶,或先或后,关系不大,而且同是一个人的活儿,因而可以合并成为:

  • 看来这是"小题大做",但在工作环节太多的时候,这样做就非常必要了

  • 这里讲的主要是时间方面的事,但在具体生产实践中,还有其他方面的许多事。这种方法虽然不一定能直接解决所有问题,但是我们利用这种方法来考虑问题,也是不无裨益的。

解法1: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
@Slf4j(topic = "c.MakeTea")
public class TestTea {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
sleep(1);
log.debug("洗水壶");
sleep(15);
log.debug("烧开水");
}, "Kyle");
Thread t2 = new Thread(() -> {
sleep(1);
log.debug("洗茶壶");
sleep(2);
log.debug("洗茶杯");
sleep(1);
log.debug("拿茶叶");
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("泡茶");
}, "Lucy");
t1.start();
t2.start();
}

static void sleep(int i) {
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
  • 结果
1
2
3
4
5
6
13:55:52.453 c.MakeTea [Lucy] - 洗茶壶
13:55:52.453 c.MakeTea [Kyle] - 洗水壶
13:55:54.458 c.MakeTea [Lucy] - 洗茶杯
13:55:55.469 c.MakeTea [Lucy] - 拿茶叶
13:56:07.463 c.MakeTea [Kyle] - 烧开水
13:56:07.463 c.MakeTea [Lucy] - 泡茶
  • 此种解法的缺陷
    • 上面模拟的是Lucy等Kyle的水烧开了,Lucy泡茶,如果现在要让Kyle等Lucy把茶叶拿过来,由Kyle泡茶呢?
    • 上面两个线程其实是各执行各的,如果要模拟Kyle把水壶交给Lucy泡茶,或者模拟Lucy把茶叶交给Kyle泡茶呢?
  • 这个缺陷我们后面会解决

共享模型之管程

共享带来的问题

小故事

  • 老王(操作系统)有一个功能强大的算盘(CPU),现在老王想把算盘租出去,赚点外快
  • 小南和小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
  • 但小南也不能一天24小时使用算盘,他得时不时小憩一会儿(sleep),又或者去吃饭上厕所(阻塞IO操作),有时候还需要一根烟,没烟的时候思路全无(wait)这些情况统称为阻塞
  • 在上面的那些情况下,算盘没利用起来,老王觉得有点不划算
  • 另外,小女也想用用算盘,如果总是让小南占着算盘,小女会觉得很不公平
  • 于是,老王灵机一动,想了个办法,让他们没人用一会儿,轮流使用算盘(CPU时间片)
  • 这样,当小南阻塞的时候,算盘可以分配给小女用,不会浪费,反之亦然
  • 最近执行的计算比较复杂,需要存储一些中间结果,而小南和小女的脑容量(工作内存)不够,所以老王申请了一个笔记本(主存),把一些中间结果先记在本上
  • 但由于分时系统,有一天还是发生了事故
  • 小南从笔记本上读取了初值0,做了一个自增+1计算,还没来得及写回结果,此时轮到小女用了
  • 于是小南嘴里念叨着,结果是1结果是1…,不甘心的上一边待着去了(上下文切换)
  • 小女此时看到笔记本上是0,于是做了一个-1的运算,并将结果-1写到笔记本上,此时小女的时间也用完了,又轮到小南了
  • 小南此时将嘴里一直念叨的1,写入了笔记本
  • 最终小南和小女都觉得自己没做错,但笔记本中的结果是1而不是0

Java的体现

  • 两个线程对一个初值为0的静态变量做自增和自减操作,各执行5000次,观察结果
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
@Slf4j(topic = "c.TestCalculate")
public class TestCalculate {
static int num = 0;

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
num++;
}
});

Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
num--;
}
});

t1.start();
t2.start();
t1.join();
t2.join();

log.debug("结果为:{}", num);
}
}
  • 多次运行,我们会发现结果有正有负,也有可能为0

问题分析

  • 为什么上面的结果不确定呢?因为Java中对静态变量的自增、自减操作并不是原子操作,这部分在我这篇文章的第二小节中做了详细的解释
  • 对于i++(i为静态变量),实际上会产生如下字节码指令
1
2
3
4
getstatic i     // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
  • 对于i--也是类似
1
2
3
4
getstatic i     // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
  • Java的内存模型如下,完成静态变量的自增、自减需要在主存与线程内存中进行数据交换
  • 出现负数的情况
1
2
3
4
5
6
7
8
9
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
  • 出现正数的情况
1
2
3
4
5
6
7
8
9
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1

临界区(Critical Section)

  • 一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时,发生指令交错,就会出现问题
  • 一段代码内如果存在对共享资源的多线程读写操作,则称这段代码为临界区

竞态条件(Race Condition)

  • 多个线程在临界区内执行,由于代码的执行序列不同,而导致结果无法预测,则称之为发生了竞态条件

synchronized解决方案

  • 为了避免临界区的竞态条件发生,有多种手段可以达到目的
    • 阻塞式的解决方案:synchronized、Lock
    • 非阻塞式的解决方案:原子变量
  • 这里使用阻塞式解决方案:synchronized,来解决上述问题,即俗称的对象锁,它采用互斥的方式让同一时刻至多有一个线程能持有对象锁,其他线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全执行临界区的代码,不用担心上下文切换。
  • 注意:虽然Java中的互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的:
    • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
    • 同步是由于线程执行的先后、顺序不同,需要一个线程等待其他线程运行到某个点
  • synchronized语法
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
@Slf4j(topic = "c.TestCalculate")
public class TestCalculate {
static int num = 0;
static final Object obj = new Object();

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (obj) {
num++;
}
}
});

Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (obj) {
num--;
}
}
});

t1.start();
t2.start();
t1.join();
t2.join();

log.debug("结果为:{}", num);
}
}
  • 可以把obj想象成一间房间(撤硕),线程t1、线程t2想象成两个人
  • 当线程t1执行到synchronized(obj)时,就好比t1进入了撤硕,并反手锁住了门,在门内执行i++操作
  • 此时如果t2也运行到了synchronized(obj),它发现门被锁住了,只能在门外等待
  • 当t1执行完synchronized块内的代码,此时才会解开门上的锁,从撤硕出来,t2线程此时才可以进入撤硕,并反手锁住门,执行它的i--操作

思考

  • synchronized实际上是使用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。为了加深理解,我们思考以下几个问题
    1. 如果把synchronized(obj)放在for循环外面,如何理解?
      • 仅在i++操作上加锁,锁住了4条虚拟机指令,但是外层循环了5W次,那就要加锁解锁5W次,这样是比较耗时的,那么此时我们就可以直接在for循环上加锁,这样就只用解锁一次
    2. 如果t1 synchronized(obj1),而t2 synchronized(obj2)会怎样运作?
      • 如果t1锁住的是obj1对象,t2锁住的是obj2对象,就好比两个人进入了两个不同的撤硕,没法起到同步的效果
    3. 如果t1 synchronized(obj),而t2没有加会怎么样?
      • 这意味着线程 t2 可以自由地对 num 进行自减操作,可能会在线程 t1 修改 num 的过程中,读取到一个中间状态的 num 值。

面向对象改进

  • 可以把需要保护的共享变量放入一个类
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
class Room {
int value = 0;

public void increment() {
synchronized (this) {
value++;
}
}

public void decrement() {
synchronized (this) {
value--;
}
}

public int get() {
synchronized (this) {
return value;
}
}
}

@Slf4j(topic = "c.TestCalculate")
public class TestCalculate {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> room.increment());

Thread t2 = new Thread(() -> room.decrement());

t1.start();
t2.start();
t1.join();
t2.join();

log.debug("结果为:{}", room.get());
}
}

方法上的synchronized

  • 我们可以直接把synchronized关键字加在方法上,这样等价于用当前对象(this)来作为锁
1
2
3
4
5
6
7
8
9
10
11
12
class Test {
public synchronized void test() {
}
}
等价于

class Test {
public void test() {
synchronized (this) {
}
}
}
  • 如果在静态方法上加synchronized,则是等价于使用该类(Test.class)作为锁
1
2
3
4
5
6
7
8
9
10
11
12
class Test {
public synchronized static void test() {
}
}
等价于

class Test {
public static void test() {
synchronized (Test.class) {
}
}
}

不加synchronized的方法

  • 不加synchronized的方法就好比不遵守规则的人,不去老实排队(好比翻墙进去的)

线程八锁

  • 其实就是考查synchronized锁住的是哪个对象,有八种情形
  1. 情况一:线程1和线程2都是用n1作为锁,二者执行顺序随机,结果为1221
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j(topic = "c.Number")
class Number {
public synchronized void a() {
log.debug("1");
}

public synchronized void b() {
log.debug("2");
}
}


public class Tmp {

public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> n1.a()).start();
new Thread(() -> n1.b()).start();
}
}
  1. 情况二:t1和t2拿到的是同一把锁,执行顺序随机,如果t1先执行,则1s后12;如果t2先执行,则2 1s后1
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
@Slf4j(topic = "c.Number")
class Number {
public synchronized void a() {
Tmp.sleep(1);
log.debug("1");
}

public synchronized void b() {
log.debug("2");
}
}


public class Tmp {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> n1.a()).start();
new Thread(() -> n1.b()).start();
}

public static void sleep(int i){
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
  1. 情况三:3 1s后1223 1s后132 1s后1
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
@Slf4j(topic = "c.Number")
class Number {
public synchronized void a() {
Tmp.sleep(1);
log.debug("1");
}

public synchronized void b() {
log.debug("2");
}

public void c() {
log.debug("3");
}
}


public class Tmp {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> n1.a()).start();
new Thread(() -> n1.b()).start();
new Thread(() -> n1.c()).start();
}

public static void sleep(int i){
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
  1. 情况四:t1和t2用的锁分别是n1和n2,相当于没锁,由于t1线程需要休眠1s,固结果为2 1s后1
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
@Slf4j(topic = "c.Number")
class Number {
public synchronized void a() {
Tmp.sleep(1);
log.debug("1");
}

public synchronized void b() {
log.debug("2");
}
}


public class Tmp {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> n1.a()).start();
new Thread(() -> n2.b()).start();
}

public static void sleep(int i){
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
  1. 情况五:t1的锁是Number.class,t2的锁是n1对象,锁不同,相当于没锁,结果同上:2 1s后1
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
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a() {
Tmp.sleep(1);
log.debug("1");
}

public synchronized void b() {
log.debug("2");
}
}


public class Tmp {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> n1.a()).start();
new Thread(() -> n1.b()).start();
}

public static void sleep(int i){
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
  1. 情况六:t1和t2的锁均为Number.class1s后122 1s后1
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
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a() {
Tmp.sleep(1);
log.debug("1");
}

public static synchronized void b() {
log.debug("2");
}
}


public class Tmp {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> n1.a()).start();
new Thread(() -> n1.b()).start();
}

public static void sleep(int i){
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
  1. 情况七:2 1s后1
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
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a() {
Tmp.sleep(1);
log.debug("1");
}

public synchronized void b() {
log.debug("2");
}
}


public class Tmp {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> n1.a()).start();
new Thread(() -> n2.b()).start();
}

public static void sleep(int i){
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
  1. 情况八:t1和t2的锁均为Number.class,与调用方n1、n2无关,结果1s后122 1s后1
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
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a() {
Tmp.sleep(1);
log.debug("1");
}

public static synchronized void b() {
log.debug("2");
}
}


public class Tmp {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> n1.a()).start();
new Thread(() -> n2.b()).start();
}

public static void sleep(int i){
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能改变,分为两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全

  • 通常情况下,方法内的局部变量是线程安全的,因为它们只能在方法内部访问,每个线程都有自己的栈帧,而局部变量就存放在栈帧内。
  • 但局部变量引用的对象未必是线程安全的
    • 如果该对象没有逃离方法的作用域,那么它是线程安全的
    • 如果该对象逃离了方法的作用范围,则需要考虑线程安全
    • 详情可以参考我这篇文章的2.2.1小结,或者在文章内Ctrl + F搜索逃离

局部变量线程安全分析

  • 示例代码
1
2
3
4
public static void test1() {
int i = 10;
i++;
}
  • 每个线程调用test1()方法时,局部变量i会在每个线程的栈帧内存中被创建多份,因此不存在共享,使用javap -v命令查看test1()的字节码
1
2
3
4
5
6
7
8
9
10
public static void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10 // 将整数10推送到操作数栈顶。
2: istore_0 // 将操作数栈顶的整数值存储到局部变量表的索引为0的位置(即将10存储到局部变量i)
3: iinc 0, 1 // 将局部变量表中索引为0的位置的整数值增加1。
Start Length Slot Name Signature
3 4 0 i I
  • 如图

  • 局部变量的引用稍有不同,我们先来看一个成员变量的例子,method2和method3都是对成员变量的修改,一个是添加元素,一个是移除元素。

    • 当多个线程执行的指令交错的时候,可能会出现list中没有元素,但是却执行了remove操作,此时就会报错
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
public class Test01 {
static final int THREAD_NUM = 2;
static final int LOOP_NUM = 200;

public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUM; i++) {
new Thread(() -> {
test.method01(LOOP_NUM);
}, "Thread" + i).start();
}
}
}

class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();

public void method01(int loopNum) {
for (int i = 0; i < loopNum; i++) {
// 临界区,会发生竞态条件
method02();
method03();
}
}

private void method02() {
list.add("1");
}

public void method03() {
list.remove(0);
}
}
  • 报错
1
2
3
4
5
6
7
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:657)
at java.util.ArrayList.remove(ArrayList.java:496)
at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35)
at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26)
at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14)
at java.lang.Thread.run(Thread.java:748)
  • 分析:无论哪个线程中的method02或method03中,引用的都是同一个对象中的list成员变量

  • 下面我们将list修改为局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ThreadSafe {

public void method01(int loopNum) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNum; i++) {
// 临界区,会发生竞态条件
method02();
method03();
}
}

private void method02(ArrayList<String> list) {
list.add("1");
}

private void method03(ArrayList<String> list) {
list.remove(0);
}
}
  • 此时无论运行多少次,也不会出现上述的问题了。因为此时list是局部变量,每个线程调用时会创建其不同的实例,没有共享

  • 我们继续从方法访问修饰符来思考,如果将method02和method03修改为public方法,会不会造成线程安全问题?

    1. 情况一:有其他线程调用method02和method03
    2. 情况二:在情况一的基础上,添加ThreadSafe的子类,子类覆盖method03方法
    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
    class ThreadSafe {

    public void method01(int loopNum) {
    ArrayList<String> list = new ArrayList<>();
    for (int i = 0; i < loopNum; i++) {
    // 临界区,会发生竞态条件
    method02(list);
    method03(list);
    }
    }

    private void method02(ArrayList<String> list) {
    list.add("1");
    }

    public void method03(ArrayList<String> list) {
    list.remove(0);
    }
    }


    class ThreadSafeSubClass extends ThreadSafe {
    @Override
    public void method03(ArrayList<String> list) {
    new Thread(() -> {
    list.remove(0);
    }).start();
    }
    }
    • 多运行几遍就会报错,原因是重写的method03方法将list共享到了新线程,造成两个线程都在修改list对象,从这个例子中可以看出private可以保护方法的线程安全的,限制子类不能覆盖它
    1
    2
    3
    4
    5
    Exception in thread "Thread-957" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
    at java.util.ArrayList.rangeCheck(ArrayList.java:659)
    at java.util.ArrayList.remove(ArrayList.java:498)
    at com.cyborg2077.demo01.ThreadSafeSubClass.lambda$method03$0(Test01.java:44)
    at java.lang.Thread.run(Thread.java:750)

常见的线程安全类

  1. String
  2. Integer
  3. StringBuffer
  4. Random
  5. Vector
  6. Hashtable
  7. java.util.concurrent包下的类
  • 这里说它们是线程安全的是指,当多个线程调用它们同一个实例的某个方法时,是线程安全的,可以理解为
1
2
3
Hashtable table = new Hashtable();
new Thread(() -> table.put("key", "value"), "t1").start();
new Thread(() -> table.put("key", "value"), "t2").start();

虽然它们的每个方法都是原子的,但多个方法的组合不是原子的

线程安全类方法的组合

  • 分析以下代码是否线程安全
1
2
3
4
5
Hashtable table = new Hashtable();
// 两个线程同时执行
if (table.get("key") == null) {
table.put("key", value);
}
  • 在多线程下,可能会发生指令交错,线程 T2 的操作结果被线程 T1 的操作结果覆盖,导致数据不一致。
1
2
3
4
T1:table.get("key") == null
T2:table.get("key") == null
T2:table.put("key", v2);
T1:table.put("key", v1);

不可变类的线程安全性

  • 在Java中,String类和Integer类被设计为不可变类(Immutable Class),这意味着一旦创建了对象,其状态就不能被修改。这种不可变性使得String和Integer对象在多线程环境中是线程安全的,因为它们的状态不会发生变化,所以不会导致线程安全问题。
    1. String类的线程安全性
      • 字符串是不可变的,一旦创建就不能修改。任何对字符串的修改都会创建一个新的字符串对象,而不会修改原始字符串对象。
      • 因为字符串不可变,所以多个线程可以同时访问同一个字符串对象,而不需要担心竞争条件或数据不一致的问题。
    2. Integer类的线程安全性
      • Integer类是一个包装类,用于封装int类型的值。它也是不可变的,一旦创建就不能修改
      • 对于常见的整数值(-128 ~ 127),Java使用IntegerCache来重用Integer对象。这意味着多个线程同时访问这些整数值时,会得到相同的Integer对象
      • 对于超出缓存范围的整数值,每个线程都会获得一个独立的Integer对象,因此不会存在竞态条件。

实例分析

  1. 例一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyServlet extends HttpServlet {
// 是否安全?否:Hashtable才是线程安全的,此map会被多个线程共享并访问到
Map<String,Object> map = new HashMap<>();
// 是否安全?是:字符串是不可变类,是线程安全的
String S1 = "...";
// 是否安全?是:理由同上
final String S2 = "...";
// 是否安全?否:会被共享,与map理由一致
Date D1 = new Date();
// 是否安全?否:final修饰符仅仅是将D2的引用值固定了,但还是可被修改的
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}
  1. 例二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyServlet extends HttpServlet {
// 是否安全?否:doGet 方法会被多个线程并发调用。由于 userService 是一个共享的成员变量,多个线程同时调用 doGet 方法可能会导致并发访问问题。
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0;

public void update() {
// ...
count++;
}
}
  • 可以将UserService对象的创建放在doGet内来完成,这样可以保证线程安全
1
2
3
4
public void doGet(HttpServletRequest request, HttpServletResponse response) {
UserService userService = new UserServiceImpl();
userService.update(...);
}
  1. 例三
  • Spring中的对象,不加@Scope(""prototype)注解声明的话,默认都是单例的,既然对象都是单例的,那么这里的start属性也是共享的,所以是线程不安全的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Aspect
@Component
public class MyAspect {
// 是否安全?
private long start = 0L;
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}

@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
  • 我们可以使用环绕通知,将start作为一个局部变量,从而避免线程安全问题
1
2
3
4
5
6
7
8
9
10
11
12
@Aspect
@Component
public class MyAspect {
@Around("execution(* *(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
Object result = joinPoint.proceed();
long end = System.nanoTime();
System.out.println("cost time: " + (end - start));
return result;
}
}
  1. 例四,从下往上看
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 MyServlet extends HttpServlet {
// 是否安全?同下,也是不可变的
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}

public class UserServiceImpl implements UserService {
// 是否安全?是:虽然UserDao是成员变量,但是里面没有东西可以改,是线程安全的,类似于无状态不可变
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}

public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全?是:没有成员变量,即使有多个线程来访问,也什么都改不了,只读
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}
  1. 例五
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 MyServlet extends HttpServlet {
// 是否安全?否
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}

public class UserServiceImpl implements UserService {
// 是否安全?否,其中的成员变量被共享,可被修改
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}

public class UserDaoImpl implements UserDao {
// 是否安全?否:Connection对象作为成员变量,会被共享
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
  1. 例六
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
public class MyServlet extends HttpServlet {
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}

public class UserServiceImpl implements UserService {
// UserDao是作为局部变量被创建的
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}

public class UserDaoImpl implements UserDao {
// 是否安全?是:虽然Connection是成员变量,但是UserDao是在update()方法中作为局部变量创建的,故这里的Connection都是每个线程独立创建的,不会被共享
private Connection = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
  1. 例七
1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Test {
public void bar() {
// 是否安全?否:虽然sdf方法是局部变量,但是sdf对象会作为参数被传递给抽象方法foo(),其子类可能会对foo()方法中的sdf做修改,相当于sdf对象逃离了方法作用范围,线程不安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}

public abstract foo(SimpleDateFormat sdf);

public static void main(String[] args) {
new Test().bar();
}
}
  • 其中foo的行为是不确定的,可能会对sdf做修改
1
2
3
4
5
6
7
8
9
10
11
12
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).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
class TicketWindow {
private int count;

public TicketWindow(int count) {
this.count = count;
}

public int getCount() {
return count;
}

/**
* 卖票
* @param amount 预购买的张数
* @return 实际卖的张数,余票不足为0
*/
public int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else
return 0;
}
}
  • 我们来实际测试一下
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
@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
public static void main(String[] args) {
TicketWindow window = new TicketWindow(1000);
ArrayList<Thread> list = new ArrayList<>();
List<Integer> sellCount = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread thread = new Thread(() -> {
int count = window.sell(new Random().nextInt(5) + 1);
sellCount.add(count);
});
list.add(thread);
thread.start();
}
list.forEach((t) -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
});
log.debug("卖出:{}张", sellCount.stream().mapToInt(n -> n.intValue()).sum());
log.debug("剩余:{}张", window.getCount());
}
}
  • 结果发生了超卖现象,原因是在售票方法处,没有加锁,导致多个线程可能同时进来修改票数
1
2
16:37:37.959 c.ExerciseSell [main] - 卖出:1008
16:37:37.962 c.ExerciseSell [main] - 剩余:0
  • 解决方案是给sell()方法加锁
1
2
3
4
5
6
7
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else
return 0;
}
  • 结果
1
2
17:55:15.759 c.ExerciseSell [main] - 卖出:1000
17:55:15.765 c.ExerciseSell [main] - 剩余:0

转账练习

  • 测试下面代码是否存在线程安全问题,并尝试改正
1
2
3
4
5
6
7
8
9
10
11
12
@Data
@AllArgsConstructor
class Account {
private int money;

public void transfer(int amount, Account target) {
if (this.money >= amount) {
this.setMoney(money - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
a.transfer(new Random().nextInt(1000), b);
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
b.transfer(new Random().nextInt(1000), a);
}
}, "t1");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("两人总金额:{}", a.getMoney() + b.getMoney());
}
}
  • 结果如下,转来转去,钱还转少了(身为银行,收点手续费怎么啦)
1
18:07:44.726 c.ExerciseTransfer [main] - 两人总金额:1436
  • 分析:这里的共享变量是money,临界区则是transfer()方法,该方法中对money进行了修改,但是没有加锁,但是直接在方法上加synchronized是没用的,这样相当于使用this当前对象来作为锁,如果多个线程同时调用transfer()方法,那么每个线程会获取不同的锁,等于没加锁
1
2
3
4
5
6
7
8
public void transfer(int amount, Account target) {
synchronized (this) {
if (this.money >= amount) {
this.setMoney(money - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
  • 正确的做法是使用相同的锁来对共享资源进行同步,这里可以使用Account.class来作为锁
1
2
3
4
5
6
7
8
public void transfer(int amount, Account target) {
synchronized (Account.class) {
if (this.money >= amount) {
this.setMoney(money - amount);
target.setMoney(target.getMoney() + amount);
}
}
}

Monitor概念

Java对象头

  • 以32位虚拟机为例,普通对象
1
2
3
4
5
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
  • 数组对象
1
2
3
4
5
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32 bits) | Klass Word(32 bits) | array length(32 bits) |
|--------------------------------|-----------------------|------------------------|
  • 其中MarkWord结构为
1
2
3
4
5
6
7
8
9
10
11
12
13
|-------------------------------------------------|----------------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------|----------------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------|----------------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------|----------------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------|----------------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------|----------------------------|
| | 11 | Marked for GC |
|-------------------------------------------------|----------------------------|
  • 64位虚拟机Mark Word
1
2
3
4
5
6
7
8
9
10
11
12
13
|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|

Monitor(锁)

  • Monitor被翻译成监视器管程
  • 每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的MarkWord中就被设置指向Monitor对象的指针
  • Monitor结构如下
  • 刚开始Monitor中Owner为null
  • 当Thread-2上锁的过程中,如果Thread-3、Thread-4、Thread-5也来执行synchronized(obj),就会进入阻塞队列等待(EntryList BLOCKED)
  • Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的
  • 图中WaitSet中的Thread-0、Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面将wait-notify时会分析
  • synchronized必须是进入同一个对象的monitor才有上述效果
  • 不加synchronized的对象不会关联监视器,不遵从以上规则

synchronized原理

  • 原理我们从字节码的角度来分析,示例代码
1
2
3
4
5
6
7
static int counter = 0;

public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
  • 使用javap -v /xx/Test.class编译后的字节码如下
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 static void main(java.lang.String[]);
// 方法签名
descriptor: ([Ljava/lang/String;)V
// 访问修饰符
flags: ACC_PUBLIC, ACC_STATIC

Code:
// 操作数栈深度和本地变量表容量
stack=2, locals=3, args_size=1
0: getstatic #2 // Field lock:Ljava/lang/Object; // 获取静态字段 lock 的值并将其推送到操作数栈顶部
3: dup // 复制栈顶的数值(即 lock 引用)并将副本推送到操作数栈顶部
4: astore_1 // 将栈顶的数值(即 lock 引用的副本)存储到本地变量 1(args 参数)
5: monitorenter // 进入监视器(锁)保护的同步块
6: getstatic #3 // Field counter:I 执行counter++
9: iconst_1
10: iadd
11: putstatic #3
14: aload_1
15: monitorexit // 自增操作完毕,退出监视器(锁)保护的同步块
16: goto 24 // 无条件跳转到指令位置 24,继续执行下面的指令
19: astore_2 // 这里是异常处理:将栈顶的数值(即异常对象引用)存储到本地变量 2(ex 异常)
20: aload_1 // 将本地变量 1(args 参数)加载到操作数栈顶部
21: monitorexit // 释放锁:在异常处理块中,退出监视器(锁)保护的同步块
22: aload_2 // 重试异常:将本地变量 2(ex 异常)加载到操作数栈顶部
23: athrow // 抛出异常
24: return // 方法返回

Exception table:
from to target type
6 16 19 any // 如果6~16行出现了异常,跳转到19行继续执行
19 22 19 any // 如果19~22行出现了异常,跳转到19行继续执行
  • 从字节码中我们可以看出,即使发生了异常,也会释放掉锁
  • 注意:方法级别的synchronized不会再字节码指令中体现

synchronized原理进阶

轻量级锁

  • 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有在竞争),那么可以使用轻量级锁来优化
  • 轻量级锁对使用者是透明的,即语法仍然是synchronized,下面我们举个例子来深入轻量级锁的原理
  • 假设有两个方法同步块,利用同一个对象加锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static final Object obj = new Object();

public static void method1() {
synchronized (obj) {
// 同步块A
method2();
}
}

public static void method2() {
synchronized (obj) {
// 同步块B
}
}
  • 首先会创建锁对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的MarkWord

  • 其次让锁记录中的Object reference,并尝试使用CAS替换Object的Mark Word,将Mark Word的值存入锁记录

  • 如果CAS替换成功,对象头汇总存储了锁记录地址和状态00,表示由该线程给对象加锁,其中00对应的是轻量级锁,如下图所示

  • 如果CAS替换失败,有两种情况

    1. 如果是其他线程已经持有了该Object的轻量级锁,表明有竞争,进入锁膨胀的过程
    2. 如果是自己执行了synchronized锁重入,那么再添加一条LockRecord作为重入的计数
  • 当退出synchronized代码块(解锁)时,如果有取值为null的锁记录,表示有重入,此时删除锁记录,表示重入计数-1

  • 当退出synchronized代码块(解锁)时,锁记录的值不为null,此时使用CAS将MarkWord的值恢复给对象头

    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁的解锁流程

锁膨胀

  • 如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这是需要进行锁膨胀,将轻量级锁变为重量级锁。
1
2
3
4
5
6
static Object obj = new Object();
public static void method1() {
synchronized(obj) {
// 同步块
}
}
  • 现在Thread-1尝试加轻量级锁
  • 由于Thread-0已经对该对象加了轻量级锁,对象的MarkWord后两位已经是00了,那么Thread-1进行CAS操作就会失败,此时进入锁膨胀流程
    • 即为Object对象申请Monitor锁,让Object指向重量级锁地址
    • 然后自己进入Monitor的EntryList BLOCKED,进入阻塞队列等待
    • 当Thread-0退出同步块解锁时,使用CAS将MarkWord的值恢复给对象头,失败。

自旋优化

  • 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持有锁的线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
  • 自旋重试成功的情况
线程 1 (core 1 上) 对象 Mark 线程 2 (core 2 上)
- 10(重量锁) -
访问同步块,获取 monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行完毕 10(重量锁)重量锁指针 自旋重试
成功(解锁) 01(无锁) 自旋重试
- 10(重量锁)重量锁指针 成功(加锁)
- 10(重量锁)重量锁指针 执行同步块
-
  • 自旋重试失败的情况
线程 1(core 1 上) 对象 Mark 线程 2(core 2 上)
- 10(重量锁) -
访问同步块,获取 monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 阻塞
-
  • 自旋会占用CPU空间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
  • 在Java6之后自旋锁是自适应的,如果对象刚刚一次自旋操作成功过,那么为这次自旋成功的可能性会很高,那就多自旋几次;反之就少自旋或者不自旋,总之是比较智能的。
  • Java7之后就不能控制是否开启自旋功能了。

偏向锁

  • 轻量级锁在没有竞争时,每次重入仍需要进行CAS操作。Java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象头的MarkWord头,之后发现这个线程ID是自己的,那么就表示没有竞争,不用CAS。以后只要不发生竞争,这个对象就归该线程所有。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static final Object obj = new Object();

public static void m1() {
synchronized (obj) {
// 同步块 A
m2();
}
}

public static void m2() {
synchronized (obj) {
// 同步块 B
m3();
}
}

public static void m3() {
synchronized (obj) {
// 同步块 C
}
}

  • 偏向状态:先来回顾一下对象头格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    |--------------------------------------------------------------------|--------------------|
    | Mark Word (64 bits) | State |
    |--------------------------------------------------------------------|--------------------|
    | unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
    |--------------------------------------------------------------------|--------------------|
    | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
    |--------------------------------------------------------------------|--------------------|
    | ptr_to_lock_record:62 | 00 | Lightweight Locked |
    |--------------------------------------------------------------------|--------------------|
    | ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
    |--------------------------------------------------------------------|--------------------|
    | | 11 | Marked for GC |
    |--------------------------------------------------------------------|--------------------|
  • 一个对象创建时:

    • 如果开启了偏向锁(默认是开启的),那么对象创建后,MarkWord的最后三位为101,此时它的thread、epoch、age都为0。
    • 偏向锁默认是延迟的,不会再程序启动时立即生效,如果想避免延迟,可以添加VM参数:XX:BiasedLockingStartupDelay=0来禁用延迟。
    • 如果没有开启偏向锁,那么对象创建后,MarkWord的最后三位为001,此时它的hashCode、age均为0,第一次用到hashCode时才会赋值。
    • 注意:偏向锁解锁后,线程ID仍存储于对象头中

wait notify

为什么需要wait

  • 由于条件不满足,小南不能继续执行计算
  • 但是小南如果一直占用着锁,等待计算资源的到来,其他人就会一直被阻塞,小南现在属于是占着茅坑不拉屎,效率太低
  • 于是老王单开了一间休息室(调用wait方法),让小南到休息室(WaitSet)等着去了,并且此时的锁被释放开,其他人可以由老王随机安排进屋
  • 知道小M将烟送来,大喊一声:你的烟到了(调用notify)方法
  • 于是小南就离开了休息室,重新进入竞争锁的队列

原理之wait/notify

  • Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
  • BLOCKED和WAITING线程都处于阻塞状态,不占用CPU时间片
  • BLOCKED线程会在Owner线程释放锁时唤醒
  • WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争

API介绍

  • obj.wait():让进入object监视器的线程到waitSet等待

  • obj.notify():让object上正在waitSet等待的线程中挑一个唤醒

  • obj.notifyAll():让object上正在waitSet等待的线程全部唤醒

  • 它们都是线程之间进行协作的手段,都属于Object对象的方法,必须获得此对象的锁,才能调用这几个方法。

  • 示例代码,创建两个线程t1和t2,分别执行wait,让二者进入到waitSet等待,在主线程中调用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
@Slf4j(topic = "c.NotifyTest")
public class Test01 {
final static Object obj = new Object();

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (obj) {
log.debug("执行...");
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("其他代码");
}
}, "t1").start();
new Thread(() -> {
log.debug("执行...");
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("其他代码");
}
}, "t2").start();

Thread.sleep(2000);
log.debug("唤醒obj上其他线程");
synchronized (obj) {
obj.notify();
}
}
}
  • 从执行结果中,我们可以看出,只有一个线程被唤醒了
1
2
3
4
14:27:53.375 c.NotifyTest [t2] - 执行...
14:27:53.375 c.NotifyTest [t1] - 执行...
14:27:55.375 c.NotifyTest [main] - 唤醒obj上其他线程
14:27:55.375 c.NotifyTest [t1] - 其他代码
  • 如何此时将主线程中的notify()修改为notifyAll(),执行结果如下
1
2
3
4
5
14:30:02.045 c.NotifyTest [t1] - 执行...
14:30:02.045 c.NotifyTest [t2] - 执行...
14:30:04.042 c.NotifyTest [main] - 唤醒obj上其他线程
14:30:04.042 c.NotifyTest [t2] - 其他代码
14:30:04.042 c.NotifyTest [t1] - 其他代码
  • 小结
    • wait()方法会释放对象的锁,进入WaitSet等待区,无限制等待,直到notify位置,并且此时其他线程有机会获取对象的锁
    • wait(long n):有时限的等待,到n毫秒后结束等待,或是被notify

wait notify 的正确姿势

  • 首先我们来看看sleep(long n)和wait(long n)的区别
    1. sleep是Thread的方法,而wait是Object的方法
    2. sleep不强制和synchronized配合使用,而wait需要和synchronized一起用
    3. sleep在睡眠的同时,不会释放对象锁,而wait在等待的时候,会释放对象锁

Step 1

  • 思考下面的解决方案好不好,为什么?
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
@Slf4j(topic = "c.TestNotifyAll")
public class Test02 {
final static Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会儿");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();

for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}).start();
}

Thread.sleep(1000);
new Thread(() -> {
hasCigarette = true;
log.debug("烟到了奥");
}, "送烟的小M").start();
}
}
  • 输出如下
1
2
3
4
5
6
7
8
9
10
14:48:08.438 c.TestNotifyAll [小南] - 有烟没?[false]
14:48:08.444 c.TestNotifyAll [小南] - 没烟,先歇会儿
14:48:09.437 c.TestNotifyAll [送烟的小M] - 烟到了奥
14:48:10.445 c.TestNotifyAll [小南] - 有烟没?[true]
14:48:10.445 c.TestNotifyAll [小南] - 可以开始干活了
14:48:10.445 c.TestNotifyAll [Thread-4] - 可以开始干活了
14:48:10.445 c.TestNotifyAll [Thread-3] - 可以开始干活了
14:48:10.445 c.TestNotifyAll [Thread-2] - 可以开始干活了
14:48:10.445 c.TestNotifyAll [Thread-1] - 可以开始干活了
14:48:10.445 c.TestNotifyAll [Thread-0] - 可以开始干活了
  • 上面的代码缺点很明显,由于小南调用的是sleep,睡眠期间会阻塞线程,其他线程不能来干活,效率太低
  • 其次,小南睡眠是固定的2s,通过观察时间戳,就算烟提前到了,他也不会起床干活
  • 送烟的线程不能加synchronized(room),因为小南一直锁着门,烟送不进去
  • 解决方案,使用wait - notify机制

Step 2

  • 思考下面的实现,感觉可行吗?
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
@Slf4j(topic = "c.TestWaitNotify")
public class Test03 {
final static Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会儿");
try {
room.wait(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();

for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}).start();
}

Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了奥");
room.notify();
}
}, "送烟的小M").start();
}
}
  • 输出
1
2
3
4
5
6
7
8
9
10
15:04:44.703 c.TestWaitNotify [小南] - 有烟没?[false]
15:04:44.708 c.TestWaitNotify [小南] - 没烟,先歇会儿
15:04:44.708 c.TestWaitNotify [Thread-4] - 可以开始干活了
15:04:44.708 c.TestWaitNotify [Thread-3] - 可以开始干活了
15:04:44.708 c.TestWaitNotify [Thread-2] - 可以开始干活了
15:04:44.708 c.TestWaitNotify [Thread-1] - 可以开始干活了
15:04:44.708 c.TestWaitNotify [Thread-0] - 可以开始干活了
15:04:45.701 c.TestWaitNotify [送烟的小M] - 烟到了奥
15:04:45.701 c.TestWaitNotify [小南] - 有烟没?[true]
15:04:45.701 c.TestWaitNotify [小南] - 可以开始干活了
  • 解决了阻塞其他线程的问题,但是如果其他线程也存在等待条件呢?

Step 3

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
@Slf4j(topic = "c.TestWaitNotifyAll")
public class Test04 {
final static Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会儿");
try {
room.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活");
}
}
}, "小南").start();

new Thread(() -> {
synchronized (room) {
log.debug("外卖到了没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会儿");
try {
room.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("外卖到了没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活");
}

}
}, "小女").start();


Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了奥");
room.notify();
}
}, "送外卖的").start();
}
}
  • 输出
1
2
3
4
5
6
7
15:18:51.925 c.TestWaitNotifyAll [小南] - 有烟没?[false]
15:18:51.931 c.TestWaitNotifyAll [小南] - 没烟,先歇会儿
15:18:51.931 c.TestWaitNotifyAll [小女] - 外卖到了没?[false]
15:18:51.931 c.TestWaitNotifyAll [小女] - 没外卖,先歇会儿
15:18:52.926 c.TestWaitNotifyAll [送外卖的小M] - 外卖到了奥
15:18:52.926 c.TestWaitNotifyAll [小南] - 有烟没?[false]
15:18:52.926 c.TestWaitNotifyAll [小南] - 没干成活
  • notify只能唤醒一个WaitSet中的线程,如果此时有其他线程也在等,那么可能唤醒不了正确的线程,这种情况被称之为虚假唤醒
    • 上面的输出结果中,外卖送到了,但是唤醒的确实小南,唤醒了错误的线程

Step 4

  • 要解决刚刚的问题,可以使用notifyAll来唤醒所有线程,但是用if + wait仅有一次判断几乎,一旦条件不成立,就没有重新判断的机会了。所以我们需要采用while + 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
@Slf4j(topic = "c.TestWaitNotifyAll")
public class Test04 {
final static Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会儿");
try {
room.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("可以开始干活了");
}
}, "小南").start();

new Thread(() -> {
synchronized (room) {
log.debug("外卖到了没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会儿");
try {
room.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("可以开始干活了");
}
}, "小女").start();


Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了奥");
room.notifyAll();
}
}, "送外卖的").start();
}
}
  • 输出
1
2
3
4
5
6
7
8
15:21:59.246 c.TestWaitNotifyAll [小南] - 有烟没?[false]
15:21:59.252 c.TestWaitNotifyAll [小南] - 没烟,先歇会儿
15:21:59.252 c.TestWaitNotifyAll [小女] - 外卖到了没?[false]
15:21:59.252 c.TestWaitNotifyAll [小女] - 没外卖,先歇会儿
15:22:00.244 c.TestWaitNotifyAll [送外卖的] - 外卖到了奥
15:22:00.244 c.TestWaitNotifyAll [小女] - 外卖到了没?[true]
15:22:00.244 c.TestWaitNotifyAll [小女] - 可以开始干活了
15:22:00.244 c.TestWaitNotifyAll [小南] - 没烟,先歇会儿
  • 唤醒所有线程,但是小南依然不符合条件,继续等待。将此种方式封装为一个模板,如下
1
2
3
4
5
6
7
8
9
10
11
synchronized(lock) {
while(条件不成立) {
lock.wait()
}
// 干活
}

// 另一个线程
synchronized(lock) {
lock.notifyAll();
}

保护性暂停模式

定义

  • 即Guarded Suspension,用在一个线程等待另一个线程的执行结果
  • 要点
    1. 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
    2. 如果有结果不断从一个线程到另一个线程,那么可以使用消息队列
    3. JDK中,join的实现,Future的实现,采用的就是此模式
    4. 因为要等待另一方的结果,因此归类到同步模式

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class GuardedObject {
private Object response;
private final Object lock = new Object();

public Object get() {
synchronized (lock) {
while (response == null) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return response;
}
}

public void complete(Object response) {
synchronized (lock) {
this.response = response;
lock.notifyAll();
}
}
}

应用

  • 一个线程等待另一个线程的执行结果,首先编写一个类来执行一些操作,下载baidu.com的网页内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Downloader {
public static List<String> download() throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL("https://www.baidu.com/").openConnection();
List<String> lines = new ArrayList<>();
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
lines.add(line);
}
}
return lines;
}
}
  • 主线程等待子线程执行下载任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j(topic = "c.TestGuarded")
public class Test05 {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(() -> {
try {
// 子线程执行下载
List<String> response = Downloader.download();
log.debug("下载完毕");
guardedObject.complete(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
},"t1").start();

log.debug("waiting...");
// 主线程阻塞等待t1线程下载完毕之后
List<String> response = (List<String>) guardedObject.get();
log.debug("获取线程执行结果:【{}】", response);
}
}
  • 执行结果
1
2
3
16:43:29.121 c.TestGuarded [main] - waiting...
16:43:30.461 c.TestGuarded [t1] - 下载完毕
16:43:30.461 c.TestGuarded [main] - 获取线程执行结果:【[<!DOCTYPE html>, <!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');, </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>]】

带超时版

  • 之前的版本没有超时时间,只能无限等待,那我们现在就要来解决这个问题
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
class GuardedObjectWithTimeout {
private Object response;
private final Object lock = new Object();

/**
* 设置超时时间
* @param timeout millis
* @return
*/
public Object get(long timeout) {
synchronized (lock) {
// 1. 记录初始时间
long begin = System.currentTimeMillis();
// 2. 已经过的时间
long passed = 0;
while (response == null) {
// 3. 设置剩余等待时间
long waitTime = timeout - passed;
// 4. 如果超时,退出等待
if (waitTime <= 0) {
break;
}
try {
lock.wait(waitTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 5. 计算已经过的时间
passed = System.currentTimeMillis() - begin;
}
return response;
}
}

public void complete(Object response) {
synchronized (lock) {
this.response = response;
lock.notifyAll();
}
}
}
  • 测试代码如下,给予一个相对充裕的超时时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j(topic = "c.TestGuarded")
public class Test06 {
public static void main(String[] args) {
GuardedObjectWithTimeout guardedObject = new GuardedObjectWithTimeout();
new Thread(() -> {
try {
List<String> response = Downloader.download();
log.debug("下载完毕");
guardedObject.complete(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
},"t1").start();

log.debug("waiting...");
// 主线程阻塞等待t1线程下载完毕之后
List<String> response = (List<String>) guardedObject.get(2000);
log.debug("获取线程执行结果:【{}】", response);
}
}
  • 顺利下载完成,并且得到结果
1
2
3
17:40:23.783 c.TestGuarded [main] - waiting...
17:40:25.645 c.TestGuarded [t1] - 下载完毕
17:40:25.645 c.TestGuarded [main] - 获取线程执行结果:【[<!DOCTYPE html>, <!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');, </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>]】
  • 将超时时间设置的短一些
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j(topic = "c.TestGuarded")
public class Test06 {
public static void main(String[] args) {
GuardedObjectWithTimeout guardedObject = new GuardedObjectWithTimeout();
new Thread(() -> {
try {
List<String> response = Downloader.download();
log.debug("下载完毕");
guardedObject.complete(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
},"t1").start();

log.debug("waiting...");
// 主线程阻塞等待t1线程下载完毕之后
List<String> response = (List<String>) guardedObject.get(500);
log.debug("获取线程执行结果:【{}】", response);
}
}
  • 结果如下,等待超时,没有获取到结果
1
2
3
17:42:52.076 c.TestGuarded [main] - waiting...
17:42:52.582 c.TestGuarded [main] - 获取线程执行结果:【null】
17:42:53.403 c.TestGuarded [t1] - 下载完毕

原理之join

  • 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
public final native boolean isAlive(); 

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;
}
}
}

多任务版GuardedObject

生产者消费者模式

park & unpark

  • 它们是LockSupport类中的方法
1
2
3
4
// 暂停当前线程
LockSupport.park();
// 回复某个线程的运行
LockSupport.unpark(暂停线程对象);
  • 先park再unpark
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j(topic = "c.TestPark")
public class Test01 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("start ..");

log.debug("park ..");
LockSupport.park();

log.debug("resume ..");
}, "t1");
t1.start();

Thread.sleep(1000);
log.debug("unpark ..");
LockSupport.unpark(t1);
}
}
  • 输出
1
2
3
4
20:11:23.497 c.TestPark [t1] - start ..
20:11:23.500 c.TestPark [t1] - park ..
20:11:24.496 c.TestPark [main] - unpark ..
20:11:24.496 c.TestPark [t1] - resume ..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j(topic = "c.TestPark")
public class Test01 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("start ..");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("park ..");
LockSupport.park();

log.debug("resume ..");
}, "t1");
t1.start();

Thread.sleep(1000);
log.debug("unpark ..");
LockSupport.unpark(t1);
}
}
  • 输出
1
2
3
4
21:36:34.445 c.TestPark [t1] - start ..
21:36:35.445 c.TestPark [main] - unpark ..
21:36:36.447 c.TestPark [t1] - park ..
21:36:36.447 c.TestPark [t1] - resume ..
  • 从结果中看到,我们先执行unpark再执行park,线程依然不会被阻塞,这是为什么呢?
    • 继续往下看park & unpark的原理
  • 与Object的wait & notify对比
    • wait、notify、notifyAll必须配合Object Monitor一起使用,而park、unpark不必
    • park & unpark是以线程为单位来阻塞和唤醒线程的,而notify只能随机唤醒一个等待中的线程,notifyAll是唤醒所有等待线程,不精确
    • park & unpark可以先unpark,而wait & notify只能不能先notify

原理之park & unpark

  • 每个线程都有自己的一个Parker对象,由三部分组成_counter_cond_mutex,打个比喻
    • 线程就像一个旅人,Parker就像他随身携带的背包,_cond就好比背包中的帐篷,_counter就好比背包中的备用干粮(0为耗尽,1为充足)
    • 调用park就是要看需不需要停下来休息
      • 如果备用干粮耗尽,那么就钻进帐篷休息
      • 如果备用干粮充足,那么就不需要停留,继续前进
    • 调用unpark,就好比令干粮充足
      • 如果此时还在帐篷,那就唤醒他继续前进
      • 如果此时线程还在运行,那么他下次调用park的时候,仅消耗掉备用干粮,不需要停留继续前进
        • 但是由于背包有限,多次调用unpark仅会补充一份备用干粮

  1. 当线程调用Unsafe.park()方法

  2. 检查_counter,此时_counter = 0,获得_mutex互斥锁

  3. 线程进入_cond条件变量阻塞

  4. 设置_counter = 0

  5. 当线程调用Unsafe.unpark()方法,设置_counter = 1

  6. 当前线程调用Unsafe.park()方法

  7. 检查_counter,此时_counter = 1,线程无需阻塞,继续执行

  8. 设置_counter = 0

重新理解线程状态转换

  • 假设有Thread t
    1. NEW -> RUNNABLE
      • 当调用t.start()方法时,由NEW -> RUNNABLE
    2. RUNNABLE <-> WAITING
      • t线程使用synchronized(obj)获取对象锁之后
      • 调用obj.wait()方法时,t线程由RUNNABLE -> WAITING
      • 调用obj.notify()obj.notifyAll()t.interrupt()
        • 竞争锁成功,t线程由WAITING -> RUNNABLE
        • 竞争锁失败,t线程由WAITING -> BLOCKED
    3. RUNNABLE <-> WAITING
      • 当线程调用t.join()方法时,当前线程会从RUNNABLE -> WAITING
        • 注意当前线程是在t线程对象的监视器上等待
      • t线程运行结束,或调用了当前线程的interrupt()时,当前线程从WAITING -> RUNNABLE
    4. RUNNABLE <-> WAITING
      • 当前线程调用LockSupport.park()方法,会让当前线程从RUNNABLE -> WAITING
      • 调用LockSupport.unpark(目标线程),会让目标现成从WAITING -> RUNNABLE
    5. RUNNABLE <-> TIMED_WAITING
      • t线程用synchronized(obj)获取了对象锁后
        • 调用obj.wait(long n)方法时,t线程从RUNNABLE -> TIMED_WAITING
        • t线程等待超过了n毫秒时,或者调用obj.notify()、obj.notifyAll()、t.interrupt()时
          • 竞争成功,t线程从TIMED_WAITING -> RUNNABLE
          • 竞争失败,t线程从TIMED_WAITING -> BLOCKED
    6. RUNNABLE <-> TIMED_WAITING
      • 当前线程调用t.join(long n)方法时,当前线程从RUNNABLE -> TIMED_WAITING
      • 当前线程等待超过了n毫秒,或t线程运行结束,或调用了当前线程的interrupt()方法时,当前线程从TIMED_WAITING -> RUNNABLE
    7. RUNNABLE <-> TIMED_WAITING
      • 当前线程调用了Thread.sleep(long n),当前线程从RUNNABLE -> TIMED_WAITING
      • 当前线程等待超过了n毫秒,当前线程从TIMED_WAITING -> RUNNABLE
    8. RUNNABLE <-> TIMED_WAITING
      • 当前线程调用LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long millis)时,当前线程从RUNNABLE -> TIMED_WAITING
      • 调用LockSupport.unpark(目标现成)或调用了线程的interrupt()或等待超时,当前线程从TIMED_WAITING -> RUNNABLE
    9. RUNNABLE <-> BLOCKED
      • t线程用synchronized(obj)获取锁时,如果竞争失败,会从RUNNABLE -> BLOCKED
      • 持有obj锁的线程的同步代码块执行完毕,会唤醒该对象上所有BLOCKED的线程重新竞争,如果其中t线程竞争成功,从BLOCKED -> RUNNABLE,其它失败的线程仍然BLOCKED
    10. RUNNABLE <-> TERMINATED
      • 当前线程所有代码运行完毕,进入TERMINATED

多把锁

  • 一间大屋子具有两个功能:睡觉和学习,这二者互不相干
  • 现在小南要学习、小女要睡觉,但是如果共用同一间屋子(仅有一把锁)的话,那么并发度很低,小南和小女是串行执行的。
  • 解决办法是准备多个房间(即准备多把锁)
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
@Slf4j(topic = "c.TestMultiLock")
public class Test02 {
static final Object lock = new Object();

public static void main(String[] args) {
new Thread(() -> {
synchronized (lock) {
log.debug("我是小南,我要睡觉");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"小南").start();

new Thread(()->{
synchronized (lock) {
log.debug("我是小女,我要学习");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"小女").start();
}
}
  • 输出结果,小南和小女的执行间隔接近1s,因为小南执行完毕后休眠了1s
1
2
16:48:50.180 c.TestMultiLock [小南] - 我是小南,我要睡觉
16:48:51.184 c.TestMultiLock [小女] - 我是小女,我要学习
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
@Slf4j(topic = "c.TestMultiLock")
public class Test02 {
static final Object sleepLock = new Object();
static final Object studyLock = new Object();

public static void main(String[] args) {
new Thread(() -> {
synchronized (sleepLock) {
log.debug("我是小南,我要睡觉");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"小南").start();

new Thread(()->{
synchronized (studyLock) {
log.debug("我是小女,我要学习");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"小女").start();
}
}
  • 采用多把锁的方式,小南和小女几乎是同时执行任务,增强了并发度
1
2
16:50:12.606 c.TestMultiLock [小南] - 我是小南,我要睡觉
16:50:12.606 c.TestMultiLock [小女] - 我是小女,我要学习
  • 将锁的粒度细分
    • 好处:可以提高并发
    • 坏处:如果一个线程需要同时获得多把锁,容易发生死锁

活跃性

死锁

  • 刚刚说,使用一个线程使用多把锁的时候,容易发生死锁,下面我们来具体分析一下这个场景
    • t1线程获得A对象锁,接下来想获取B对象锁。
    • t2线程获得B对象锁,接下来想获得A对象锁。
    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
    @Slf4j(topic = "c.TestDeadlock")
    public class Test03 {
    static final Object A = new Object();
    static final Object B = new Object();

    public static void main(String[] args) {
    new Thread(() -> {
    synchronized (A) {
    log.debug("lock A");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    synchronized (B) {
    log.debug("lock B");
    }
    }
    }, "t1").start();

    new Thread(() -> {
    synchronized (B) {
    log.debug("lock B");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    synchronized (A) {
    log.debug("lock A");
    }
    }
    }, "t2").start();
    log.debug("A:{}", A);
    log.debug("B:{}", B);
    }
    }
    • 结果如下,产生了死锁
    1
    2
    3
    4
    20:16:42.469 c.TestDeadlock [main] - A:java.lang.Object@2aafb23c
    20:16:42.469 c.TestDeadlock [t2] - lock B
    20:16:42.469 c.TestDeadlock [t1] - lock A
    20:16:42.473 c.TestDeadlock [main] - B:java.lang.Object@2eee9593

定位死锁

  • 检测死锁可以使用jconsole工具,或者使用jps定位进程id,然后使用jstack定位死锁。我这里就用jconsole来检测死锁了

  • 从上面的分析中,我们可以看出
    • 线程 t1 正在等待获取 java.lang.Object@2eee9593 对象的锁,但它被线程 t2 拥有,通过我们的日志输出,java.lang.Object@2eee9593就是B对象。
    • 线程 t2 正在等待获取 java.lang.Object@2aafb23c 对象的锁,但它被线程 t1 拥有,通过我们的日志输出,java.lang.Object@2aafb23c就是A对象。

哲学家就餐问题

  • 有五位哲学家,分别是苏格拉底、柏拉图、亚里士多德、赫拉克利特、阿基米德,围坐在圆桌旁

    • 他们只做两件事,思考和吃饭,思考一会儿吃口饭,吃完饭继续思考
    • 吃饭时要用两根筷子吃,桌上共有5根筷子,每位哲学家左右手边各有一根筷子。
    • 如果筷子被身边人拿着,自己就得等待
  • 那么现在我们就来模拟一下这个场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Chopstick {
String name;

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

@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}
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
@Slf4j(topic = "c.Philosopher")
public class Philosopher extends Thread {
Chopstick left;
Chopstick right;

public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}

private void eat() {
log.debug("我踏马吃吃吃");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

@Override
public void run() {
while (true) {
// 拿左手筷子
synchronized (left) {
// 拿右手筷子
synchronized (right) {
eat();
}
// 放下右手筷子
}
// 放下左手筷子
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test04 {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");

new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
  • 执行不一会儿,就执行不下去了
1
2
3
4
5
6
21:03:57.796 c.Philosopher [苏格拉底] - 我踏马吃吃吃
21:03:57.796 c.Philosopher [亚里士多德] - 我踏马吃吃吃
21:03:58.804 c.Philosopher [柏拉图] - 我踏马吃吃吃
21:03:59.804 c.Philosopher [柏拉图] - 我踏马吃吃吃
21:04:00.806 c.Philosopher [柏拉图] - 我踏马吃吃吃
21:04:01.816 c.Philosopher [苏格拉底] - 我踏马吃吃吃
  • 使用jconsole来检测一下是否发生了死锁
  • 确实是发生了死锁
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
名称: 阿基米德
状态: com.cyborg2077.demo03.Chopstick@1eadee3上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 1, 总等待数: 0

堆栈跟踪:
com.cyborg2077.demo03.Philosopher.run(Philosopher.java:32)
- 已锁定 com.cyborg2077.demo03.Chopstick@45bda0d0
-------------------------------------------------------------------------
名称: 苏格拉底
状态: com.cyborg2077.demo03.Chopstick@754b4f67上的BLOCKED, 拥有者: 柏拉图
总阻止数: 8, 总等待数: 2

堆栈跟踪:
com.cyborg2077.demo03.Philosopher.run(Philosopher.java:32)
- 已锁定 com.cyborg2077.demo03.Chopstick@1eadee3
-------------------------------------------------------------------------
名称: 柏拉图
状态: com.cyborg2077.demo03.Chopstick@18f3778b上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 3, 总等待数: 3

堆栈跟踪:
com.cyborg2077.demo03.Philosopher.run(Philosopher.java:32)
- 已锁定 com.cyborg2077.demo03.Chopstick@754b4f67
-------------------------------------------------------------------------
名称: 亚里士多德
状态: com.cyborg2077.demo03.Chopstick@68d84ce6上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 7, 总等待数: 2

堆栈跟踪:
com.cyborg2077.demo03.Philosopher.run(Philosopher.java:32)
- 已锁定 com.cyborg2077.demo03.Chopstick@18f3778b
-------------------------------------------------------------------------
名称: 赫拉克利特
状态: com.cyborg2077.demo03.Chopstick@45bda0d0上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0

堆栈跟踪:
com.cyborg2077.demo03.Philosopher.run(Philosopher.java:32)
- 已锁定 com.cyborg2077.demo03.Chopstick@68d84ce6
  • 现在一人手里一根筷子,都在等待对方释放资源,线程执行不下去了
  • 解决办法继续看后面的可重入锁

活锁

  • 活锁出现在两个线程互相改变对方的结束条件,最终谁也无法结束,例如现在有一个变量为10,一个线程要将其自减至0才结束,另一个线程要将其自增至20才结束
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
@Slf4j(topic = "c.TestLiveLock")
public class Test05 {
static volatile int count = 10;

public static void main(String[] args) {
new Thread(() -> {
while (count > 0) {
count--;
log.debug("count:{}", count);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "t1").start();

new Thread(() -> {
while (count < 20) {
count++;
log.debug("count:{}", count);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "t2").start();
}
}
  • 从结果中可以看到,两个线程一直在僵持,互相改变对方的结束条件,最后谁也无法结束
1
2
3
4
5
6
7
21:14:38.140 c.TestLiveLock [t2] - count11
21:14:38.140 c.TestLiveLock [t1] - count10
21:14:38.342 c.TestLiveLock [t1] - count11
21:14:38.342 c.TestLiveLock [t2] - count12
21:14:38.544 c.TestLiveLock [t1] - count10
21:14:38.544 c.TestLiveLock [t2] - count11
21:14:38.747 c.TestLiveLock [t2] - count12

饥饿

  • 如果一个线程由于优先级过低,始终得不到CPU调度执行,也不能够结束。

ReentrantLock

  • 相较于synchronized,可重入锁具备如下特点
    1. 可中断
    2. 可以设置超时时间:ReentrantLock提供了tryLock(long timeout, TimeUnit unit)方法,允许线程在尝试获取锁时设定一个超时时间,在指定的时间内未能获取锁,则可以执行其他逻辑或放弃锁请求。
    3. 可以设置为公平锁:保证多个等待锁的线程按照申请锁的顺序获得锁,避免线程饥饿现象。
    4. 支持多个条件变量:ReentrantLock通过Condition接口支持多个条件变量。一个ReentrantLock对象可以绑定多个Condition实例,每个Condition对象可以让一组线程等待或唤醒,从而实现更灵活的线程通信。就好比synchronized只给不满足条件的线程只提供了一间休息室,唤醒时只能notify随机唤醒一个或者notifyAll唤醒全部,而ReentrantLock提供了多间休息室,并且可以按休息室来唤醒。
  • 基本语法
1
2
3
4
5
6
7
8
9
ReentrantLock reentrantLock = new ReentrantLock();
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    @Slf4j(topic = "c.TestReentrantLock")
    public class Test06 {
    static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) {
    method1();
    }

    public static void method1() {
    reentrantLock.lock();
    try {
    log.debug("execute method1");
    method2();
    } finally {
    reentrantLock.unlock();
    }
    }

    public static void method2() {
    reentrantLock.lock();
    try {
    log.debug("execute method2");
    method3();
    } finally {
    reentrantLock.unlock();
    }
    }

    public static void method3() {
    reentrantLock.lock();
    try {
    log.debug("execute method3");
    } finally {
    reentrantLock.unlock();
    }
    }
    }
    • 输出
    1
    2
    3
    21:58:57.369 c.TestReentrantLock [main] - execute method1
    21:58:57.373 c.TestReentrantLock [main] - execute method2
    21:58:57.373 c.TestReentrantLock [main] - execute method3

可打断

  • 示例代码如下,先让主线程获取锁,然后启动t1线程,t1线程内也尝试获取锁,但由于此时锁的持有者是主线程,所以t1线程会阻塞等待,然后主线程执行打断
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
@Slf4j(topic = "c.TestInterrupt")
public class Test07 {

public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动");
try {
reentrantLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断了");
return;
}
try {
log.debug("获得了锁");
} finally {
reentrantLock.unlock();
}
}, "t1");

reentrantLock.lock();
log.debug("获得了锁");
t1.start();
try {
Thread.sleep(1000);
t1.interrupt();
log.debug("执行打断");
} finally {
reentrantLock.unlock();
}
}
}
  • 输出
1
2
3
4
5
6
7
8
9
10
10:13:13.081 c.TestInterrupt [main] - 获得了锁
10:13:13.089 c.TestInterrupt [t1] - 启动
10:13:14.089 c.TestInterrupt [main] - 执行打断
10:13:14.090 c.TestInterrupt [t1] - 等锁的过程中被打断了
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.cyborg2077.demo03.Test07.lambda$main$0(Test07.java:15)
at java.lang.Thread.run(Thread.java:750)

锁超时

  • 立刻失败
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
@Slf4j(topic = "c.TestTryLock")
public class Test08 {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动");
if (!reentrantLock.tryLock()) {
log.debug("获取锁失败,立刻返回");
return;
}
try {
log.debug("获取了锁");
} finally {
log.debug("释放了锁");
reentrantLock.unlock();
}
},"t1");

reentrantLock.lock();
log.debug("获取了锁");
t1.start();
try {
Thread.sleep(2000);
} finally {
log.debug("释放了锁");
reentrantLock.unlock();
}
}
}
  • 输出
1
2
3
4
10:31:14.310 c.TestTryLock [main] - 获取了锁
10:31:14.313 c.TestTryLock [t1] - 启动
10:31:14.313 c.TestTryLock [t1] - 获取锁失败,立刻返回
10:31:16.313 c.TestTryLock [main] - 释放了锁
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
@Slf4j(topic = "c.TestTryLock")
public class Test08 {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动");
try {
if (!reentrantLock.tryLock(3, TimeUnit.SECONDS)) {
log.debug("等待1s后获取失败,返回");
return;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
log.debug("获取了锁");
} finally {
log.debug("释放了锁");
reentrantLock.unlock();
}
},"t1");

reentrantLock.lock();
log.debug("获取了锁");
t1.start();
try {
Thread.sleep(2000);
} finally {
log.debug("释放了锁");
reentrantLock.unlock();
}
}
}
  • 输出
1
2
3
4
10:30:09.663 c.TestTryLock [main] - 获取了锁
10:30:09.666 c.TestTryLock [t1] - 启动
10:30:10.667 c.TestTryLock [t1] - 获取等待1s后失败,返回
10:30:11.667 c.TestTryLock [main] - 释放了锁

解决哲学家就餐问题

  • 哲学家就餐之所以会出现死锁,是因为一人手里一根筷子,都在等别人放下筷子,那么我们使用刚刚的tryLock来设置一个等待时间,到时自动放下筷子就好了
  • 修改之前的代码,让筷子类继承ReentrantLock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Chopstick extends ReentrantLock {
String name;

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

@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}
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
@Slf4j(topic = "c.Philosopher")
public class Philosopher extends Thread {
Chopstick left;
Chopstick right;

public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}

private void eat() {
log.debug("我踏马吃吃吃");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

@Override
public void run() {
while (true) {
// 尝试获取左手筷子
if (left.tryLock()) {
try {
// 尝试获取右手筷子
if (right.tryLock()) {
// 吃完先放下 右手筷子
try {
eat();
} finally {
right.unlock();
}
}
// 没拿到右手筷子或者吃完了,把左手筷子也放了
} finally {
left.unlock();
}
}
}
}
}
  • 此类不需要做修改,现在再执行,五位哲学家就可以正常吃饭了,不会发生死锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test04 {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");

new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}

条件变量

  • ReentrantLock支持多个条件变量,就好比

    • synchronized是那些不满足条件的线程都在同一间休息室等消息
    • ReentrantLock支持多间休息室,有专门等烟的休息室、专门等外卖的休息室、唤醒时也是按休息室来唤醒
  • 使用要点

    1. await前需要获得锁
    2. await执行后,会释放锁,进入conditionObject等待
    3. await的线程可以通过conditionObject的signal()方法来被唤醒去重新竞争lock锁,await的方法被打断或者超时的时候,也会去重新竞争lock锁
    4. 竞争lock锁成功后,从await后继续执行
  • 例子

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
@Slf4j(topic = "c.TestCondition")
public class Test09 {
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitTakeoutQueue = lock.newCondition();
static volatile boolean hasCigarette = false;
static volatile boolean hasTakeout = false;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
lock.lock();
while (!hasCigarette) {
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("拿到了烟");
}
} finally {
lock.unlock();
}
}, "小南").start();

new Thread(() -> {
try {
lock.lock();
while (!hasTakeout) {
try {
waitTakeoutQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("拿到了外卖");
} finally {
lock.unlock();
}
}, "小女").start();

Thread.sleep(1000);
sendCigarette();
Thread.sleep(1000);
sendTakeout();

}

private static void sendCigarette() {
lock.lock();
try {
log.debug("送烟的来了");
hasCigarette = true;
waitCigaretteQueue.signal();
} finally {
lock.unlock();
}
}

private static void sendTakeout() {
lock.lock();
try {
log.debug("送外卖的来了");
hasTakeout = true;
waitTakeoutQueue.signal();
} finally {
lock.unlock();
}
}
}
  • 输出
1
2
3
4
11:58:19.515 c.TestCondition [main] - 送烟的来了
11:58:19.518 c.TestCondition [小南] - 拿到了烟
11:58:20.519 c.TestCondition [main] - 送外卖的来了
11:58:20.519 c.TestCondition [小女] - 拿到了外卖

同步模式之顺序控制

固定运行顺序

  • 例如必须先执行线程2再执行线程1
    • 使用wait notify来解决,使用此种凡事,需保证先wait再notify,否则wait线程永远得不到唤醒。因此需要设立了一个isT2Ran字段来判断该不该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
    @Slf4j(topic = "c.WaitNotify")
    public class Test01 {
    static Object obj = new Object();
    static boolean isT2Ran = false;

    public static void main(String[] args) {
    new Thread(() -> {
    synchronized (obj) {
    // 如果t2没有执行过
    while (!isT2Ran) {
    try {
    // 那么t1等待
    obj.wait();
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }
    log.debug("1");
    }
    }, "t1").start();

    new Thread(() -> {
    log.debug("2");
    synchronized (obj) {
    // t2执行的时候修改标记
    isT2Ran = true;
    // 并且唤醒等待的线程
    obj.notifyAll();
    }
    }, "t2").start();

    }
    }
    • 除此之外还可以使用park和unpark来解决,代码更为简洁,这里的unpark的作用于设立的isT2Ran的作用类似,都可以起到通知t1线程,t2线程已经执行过了的作用。使用此种方式更为灵活,无论t1线程和t2线程的调用顺序如何,都是以线程为单位暂停和恢复,不需要同步对象(obj)和运行标记(isT2Ran)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Slf4j(topic = "c.ParkUnpark")
    public class Test02 {
    public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
    // 当t1线程没有许可的时候,t1线程会暂停,有许可的时候,用掉这个许可,恢复当前线程执行
    LockSupport.park();
    log.debug("1");
    }, "t1");

    Thread t2 = new Thread(() -> {
    // 给t1线程发放许可
    LockSupport.unpark(t1);
    log.debug("2");
    }, "t2");

    t1.start();
    t2.start();
    }
    }

交替输出

  • t1输出a五次,t2输出b五次,t3输出c五次
    • 使用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
    41
    42
    /**
    * 输出内容 等待标记 下一个标记
    * a 1 2
    * b 2 3
    * c 3 1
    */
    @Slf4j(topic = "c.SyncWaitNotify")
    public class SyncWaitNotify {
    // 等待标识
    private int flag;
    // 循环次数
    private int loopNum;

    public SyncWaitNotify(int flag, int loopNum) {
    this.flag = flag;
    this.loopNum = loopNum;
    }

    /**
    *
    * @param str 输出内容
    * @param waitFlag 等待标记
    * @param nextFlag 下一个标记
    */
    public void print(String str, int waitFlag, int nextFlag) {
    for (int i = 0; i < loopNum; i++) {

    synchronized (this) {
    while (flag != waitFlag) {
    try {
    this.wait();
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }
    log.debug(str);
    flag = nextFlag;
    this.notifyAll();
    }
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Test03 {

    public static void main(String[] args) {
    SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
    new Thread(() -> {
    syncWaitNotify.print("a", 1, 2);
    }, "t1").start();

    new Thread(() -> {
    syncWaitNotify.print("b", 2, 3);
    }, "t2").start();

    new Thread(() -> {
    syncWaitNotify.print("c", 3, 1);
    }, "t3").start();
    }
    }

    输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    17:50:51.373 c.SyncWaitNotify [t1] - a
    17:50:51.376 c.SyncWaitNotify [t2] - b
    17:50:51.376 c.SyncWaitNotify [t3] - c
    17:50:51.376 c.SyncWaitNotify [t1] - a
    17:50:51.376 c.SyncWaitNotify [t2] - b
    17:50:51.376 c.SyncWaitNotify [t3] - c
    17:50:51.376 c.SyncWaitNotify [t1] - a
    17:50:51.376 c.SyncWaitNotify [t2] - b
    17:50:51.376 c.SyncWaitNotify [t3] - c
    17:50:51.376 c.SyncWaitNotify [t1] - a
    17:50:51.376 c.SyncWaitNotify [t2] - b
    17:50:51.376 c.SyncWaitNotify [t3] - c
    17:50:51.376 c.SyncWaitNotify [t1] - a
    17:50:51.376 c.SyncWaitNotify [t2] - b
    17:50:51.376 c.SyncWaitNotify [t3] - c
    • 使用await和signal来解决
    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
    public class SyncAwaitSignal {
    public static void main(String[] args) {
    AwaitSignal awaitSignal = new AwaitSignal(5);
    Condition a = awaitSignal.newCondition();
    Condition b = awaitSignal.newCondition();
    Condition c = awaitSignal.newCondition();
    new Thread(() -> {
    awaitSignal.print("a", a, b);
    }, "t1").start();
    new Thread(() -> {
    awaitSignal.print("b", b, c);
    }, "t2").start();
    new Thread(() -> {
    awaitSignal.print("c", c, a);
    }, "t3").start();

    awaitSignal.start(a);
    }
    }

    @Slf4j(topic = "c.AwaitSignal")
    class AwaitSignal extends ReentrantLock {

    private int loopNum;

    public AwaitSignal(int loopNum) {
    this.loopNum = loopNum;
    }

    public void print(String str, Condition current, Condition next) {
    for (int i = 0; i < loopNum; i++) {
    this.lock();
    try {
    // 每个线程都先进自己的休息室等待
    current.await();
    log.debug(str);
    // 叫醒下一个
    next.signal();
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    } finally {
    this.unlock();
    }
    }
    }

    public void start(Condition condition) {
    this.lock();
    try {
    condition.signal();
    } finally {
    this.unlock();
    }
    }
    }
    • 使用Park和Unpark来解决
    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
    public class SyncParkUnpark {
    static Thread t1;
    static Thread t2;
    static Thread t3;

    public static void main(String[] args) {
    ParkUnpark parkUnpark = new ParkUnpark(5);
    t1 = new Thread(() -> {
    parkUnpark.print("a", t2);
    }, "t1");
    t2 = new Thread(() -> {
    parkUnpark.print("b", t3);

    }, "t2");
    t3 = new Thread(() -> {
    parkUnpark.print("c", t1);
    }, "t3");

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

    LockSupport.unpark(t1);
    }
    }

    @Slf4j(topic = "c.ParkUnpark")
    class ParkUnpark {
    private int loopNum;

    public ParkUnpark(int loopNum) {
    this.loopNum = loopNum;
    }

    public void print(String str, Thread next) {
    for (int i = 0; i < loopNum; i++) {
    LockSupport.park();
    log.debug(str);
    LockSupport.unpark(next);
    }
    }
    }

小结

  • 通过上面的学习,需要掌握的是
    • 分析多线程访问共享资源时,哪些代码片段属于临界区
    • 使用synchronized互斥解决临界区的线程安全问题
      • 掌握synchronized锁对象方法
      • 掌握synchronized加载成员方法和静态方法语法
      • 掌握wait/notify同步方法
    • 使用lock互斥锁解决临界区线程安全问题
      • 掌握lock使用细节:可打断、锁超时、公平锁、条件变量
    • 学会分析变量的线程安全性、常见的线程安全类的使用
    • 了解线程活跃性问题:死锁、活锁、饥饿
    • 应用方面
      • 互斥:使用synchronized或Lock达到共享资源互斥效果
      • 同步:使用wait/notify或Lock的条件变量达到线程间通信效果
    • 原理方面
      • monitor、synchronized、wait/notify原理
      • synchronized进阶原理
      • park/unpark原理
    • 模式方面
      • 同步模式之保护性暂停
      • 异步模式之生产者消费者
      • 同步模式之顺序控制

共享模型之内存

  • 上一小节主要讲解的Monitor主要关注的是访问共享变量时,保证临界区代码的原子性
  • 在这一小节我们来进一步深入学习共享变量在多线程间的可见性问题和命令执行时的有序性问题

Java内存模型

  • JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
  • JMM 体现在以下几个方面
    1. 原子性:保证指令不会收到线程上下文切换的影响
    2. 可见性:保证指令不会受CPU缓存的影响
    3. 有序性:保证指令不会受CPU指令并行优化的影响

可见性

退不出的循环

  • 先来看一个现象,main线程对run变量的修改对于t线程不可见,导致t线程无法停止,日志迟迟没有打印结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j(topic = "c.TestRun")
public class Test01 {
static boolean flag = true;

public static void main(String[] args) {
new Thread(() -> {
while (flag) {

}
log.debug("结束");
}).start();
log.debug("start");
sleep(1);
flag = false;
}
}
  • 为什么会出现这种状况呢?我们来分析一下
    1. 初始状态:t线程刚开始,从主存中读取flag到工作内存
    2. 因为t线程要频繁地从主存中读取flag的值,所以JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中flag的访问,提高效率
    3. 1s后,主线程修改了flag的值为false,并同步至主存,而t线程则是继续从自己的工作内存中读取的flag的值,始终为旧值
    • 主存就是所有共享信息存储的位置,工作内存是每个线程私有信息存储的位置

解决方法

  • volatile(易变关键字):它可以用来修饰成员变量和静态成员变量,避免线程从自己的工作内存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test01 {
static volatile boolean flag = true;

public static void main(String[] args) {
new Thread(() -> {
while (flag) {

}
log.debug("结束");
}).start();
log.debug("start");
sleep(1);
flag = false;
}
}

可见性 vs 原子性

  • 前面的例子体现的就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改是对另一个线程可见的,但不保证原子性,仅用于在一个写线程,多个读线程的情况,从字节码的角度看是这样的
1
2
3
4
5
6
getstatic flag // 线程 t 获取 flag true
getstatic flag // 线程 t 获取 flag true
getstatic flag // 线程 t 获取 flag true
getstatic flag // 线程 t 获取 flag true
putstatic flag // 线程 main 修改 flag 为 false
getstatic flag // 线程 t 获取 flag false
  • 对比一下之前讲线程安全时的例子,两个线程,一个进行自增,一个进行自减,只能保证看到最新值,但不能解决指令交错
1
2
3
4
5
6
7
8
9
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
  • 但是synchronized代码块既可以保证代码块的原子性,又可以保证代码块内变量的可见性。但是缺点是synchronized是属于重量级操作,性能相对较低。

两阶段终止模式-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
31
32
33
34
35
36
37
38
@Slf4j(topic = "c.TestTPTVolatile")
public class Test02 {
public static void main(String[] args) throws InterruptedException {
TPTVolatile tpt = new TPTVolatile();
tpt.start();
Thread.sleep(3500);
log.debug("停止监控");
tpt.stop();
}
}

@Slf4j(topic = "c.TPTVolatile")
class TPTVolatile {
private Thread thread;
private volatile boolean flag = false;

public void start() {
thread = new Thread(() -> {
while (true) {
if (flag) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("执行监控记录");
} catch (InterruptedException e) {
}
}
}, "监控线程");
thread.start();
}

public void stop() {
flag = true;
thread.interrupt();
}
}
  • 输出
1
2
3
4
5
13:06:40.611 c.TPTVolatile [监控线程] - 执行监控记录
13:06:41.614 c.TPTVolatile [监控线程] - 执行监控记录
13:06:42.616 c.TPTVolatile [监控线程] - 执行监控记录
13:06:43.113 c.TestTPTVolatile [main] - 停止监控
13:06:43.113 c.TPTVolatile [监控线程] - 料理后事

犹豫模式

定义

  • Balking(犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回

实现

  • 其实实现起来非常简单,只需要增加一个字段来标识这件事已经有人做了,继续拿上面的监控线程举例
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
@Slf4j(topic = "c.TestTPTVolatile")
public class Test02 {
public static void main(String[] args) throws InterruptedException {
TPTVolatile tpt = new TPTVolatile();
tpt.start();
+ tpt.start();
Thread.sleep(3500);
log.debug("停止监控");
tpt.stop();
}
}

@Slf4j(topic = "c.TPTVolatile")
class TPTVolatile {
private Thread thread;

private volatile boolean flag = false;

+ private boolean starting = false;

public void start() {
+ if (starting) {
+ log.debug("监控线程已启动,直接结束返回");
+ return;
+ }
log.debug("启动监控线程");
+ starting = true;
thread = new Thread(() -> {
while (true) {
if (flag) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("执行监控记录");
} catch (InterruptedException e) {
}
}
}, "监控线程");
thread.start();
}

public void stop() {
flag = true;
thread.interrupt();
}
}
  • 输出
1
2
3
4
5
6
7
16:26:59.682 c.TPTVolatile [main] - 启动监控线程
16:26:59.730 c.TPTVolatile [main] - 监控线程已启动,直接结束返回
16:27:00.731 c.TPTVolatile [监控线程] - 执行监控记录
16:27:01.732 c.TPTVolatile [监控线程] - 执行监控记录
16:27:02.736 c.TPTVolatile [监控线程] - 执行监控记录
16:27:03.231 c.TestTPTVolatile [main] - 停止监控
16:27:03.231 c.TPTVolatile [监控线程] - 料理后事
  • 它还经常用来实现线程安全的单例
1