若文章内容或图片失效,请留言反馈。
部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
- 视频链接:https://www.bilibili.com/video/av81461839
- 配套资料:https://pan.baidu.com/s/1lSDty6-hzCWTXFYuqThRPw( 提取码:5xiu)
写这篇博客旨在制作笔记,方便个人在线阅览,巩固知识。无他用。
博客的内容主要来自视频内容和资料中提供的学习笔记。当然,在此基础之上也增删了一些内容。
参考书籍
- 《实战 JAVA 高并发程序设计》 葛一鸣 著
- 《JAVA 并发编程实战》 Brian Goetz 等 著
- 《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
参考博客
- Java并发编程(中上篇)从入门到深入 超详细笔记
系列目录
- 学习笔记:Java 并发编程①_基础知识入门
- 学习笔记:Java 并发编程②_共享模型之管程
内容概览
- 共享模型之管程
- 共享问题(临界区、竞态条件)
- synchroinzed(synchorinized 作用于对象和方法)
- 线程安全分析(成员变量、局部变量、局部变量引用、线程安全类)
- Monitor(Java 对象头、Monitor 原理、synchorinized 原理、锁优化)
- wait / notify(原理、API、正确使用姿势)
- 设计模式-1(保护性暂停、join 原理、生产者消费者)
- Park / Unpark(实现、特性、原理)
- 重新理解线程状态的转换
- 活跃性(死锁、活锁、饥饿)
- ReentrantLock(可重入、可中断、可设置超时件时间、可设置为公平锁、支持多个变量条件)
- 设计模式-2(同步模式之顺序控制:固定运行顺序、交替输出)
1.基础知识回顾
- 同步(Synchronous)和异步(Asynchronous)
- 并发(Concurrency)和并行(Parallelism)
- 临界区(Critical Section)
- 阻塞(Blocking)和非阻塞(Non-Blocking)
- 死锁(Deadlock)、饥饿(Starvation)、活锁(Livelock)
- Java 线程的状态转换(New、Runnable、Waiting、Timed Waiting、Blocked、Terminated)
- 操作系统层面的线程状态转换
1.1.同步和异步
同步和异步通常用来形容一次方法调用。
同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
异步方法更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。
1.2.并发和并行
并发和并行是两个非常容易被混淆的概念。它们都可以表示两个或者多个任务一起执行,但侧重点有所不同。
并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。并行是真正意义上的 “同时执行”。
从严格意义上来说,并行的多个任务是真的同时执行;
而对于并发来说,这个过程是交替的,一会儿执行任务 A,一会儿执行任务 B,系统会不停地在二者间切换。
但对于外部观察者来说,即使多个任务间是串行并发的,也会产生多任务并行执行的错觉。
1.3.临界区
临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用。
但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源就必须等待。
在并行程序中,临界区资源是保护的对象。
1.4.阻塞和非阻塞
阻塞和非阻塞通常用来形容多线程间的相互影响。
比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在临界区中等待。
等待会引起线程挂起,这种情况就是线程阻塞。
此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区的线程都不能工作。
非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断向前执行。
1.5.死锁、饥饿、活锁
死锁、饥饿、活锁都属于多线程的活跃性问题。
死锁是最糟糕的一种情况。
当一个线程永远的持有一个锁,并且其他线程都尝试获得这个锁的时候,它们将永远被阻塞。
这种情况就是最简单的死锁形式,其中多个线程由于存在环路的锁依赖关系而永远地等待下去。
饥饿是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。
比如,它的优先级可能太低了,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。
此外,某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。
与死锁相比,饥饿还是有可能在未来一段时间内解决的(比如高优先级的线程已经完成任务,不再疯狂执行)。
活锁不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。
活锁通常发生在处理事务消息的应用程序中:
如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的开头。
如果消息处理器在处理某种特定类型的消息时存在错误并导致它失败,那么每当这个消息从队列中取出并传递到存在错误的处理器时,都会发生事务回滚。由于这条消息又被放到队列开头,因此处理器将被反复调用,并返回相同的结果。虽然处理消息的线程没有阻塞,但也无法继续执行下去。这种形式的活锁通常是由过度的错误恢复代码造成的,因为它错误地将不可修复的错误作为可修复的错误。
当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行的时候,就发生了活锁。
1.6.Java 线程的状态转换
Java 线程的状态转换
Java 语言定义了 6 种线程状态。
在任意一个时间点中,一个线程只能有且只有其中的一种状态,并且可以通过特定的方法在不同状态之间转换。
这 6 种状态分别是:New、Runnable、Waiting、Timed Waiting、Blocked、Terminated
/**
* 线程中的所有状态都在 Thread 中的 State 枚举中定义
*/
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- 新建(New):创建后尚未启动的线程处于这种状态。
- 当一个线程对象被创建,但还未调用 start() 方法时处于 New 状态。此时未与操作系统底层线程关联。
- 不要用 run() 方法来开启线程,它只会在当前线程中串行执行 run() 方法中的代码。
- 可运行(Runnable):包括操作系统线程状态中的 Running 和 Ready。
也就是处于此状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间。- 调用了 start() 方法,就会由 New 进入 Runnable。此时与底层线程关联,由操作系统调度执行。
- 无限期等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线程显式唤醒。
- 以下方法会让线程陷入无限期的等待状态:
- 没有设置 Timeout 参数的 Object::wait() 方法;
- 没有设置 Timeout 参数的 Thread::join() 方法;
- LockSupport::park() 方法。
- 获取锁成功后,因为条件不满足,调用了 wait() 方法,此时从 可运行 状态释放锁进入 Monitor 等待集合 等待,同样不占用 cpu 时间;
- 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的 Waiting 线程,恢复为 Runnable 状态
- 以下方法会让线程陷入无限期的等待状态:
- 限期等待(Timed Waiting):处于这种状态的线程也不会被分配处理器执行时间。
不过无须等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。- 以下方法会让线程进入限期等待状态:
- Thread::sleep() 方法;
- 设置了 Timeout 参数的 Object::wait() 方法;
- 设置了 Timeout 参数的 Thread::join() 方法;
- LockSupport::parkNanos() 方法;
- LockSupport::parkUntil() 方法。
- 获取锁成功后,因为条件不满足,调用了 wait(long) 方法,此时从 Runnable 状态释放锁进入 Monitor 等待集合进行 Timed Waiting,同样不占用 cpu 时间
- 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的 Timed Waiting 线程,恢复为 Runnable 状态,并重新去竞争锁
- 如果等待超时,也会从 Timed Waiting 状态恢复为 Runnable 状态,并重新去竞争锁
- 还有一种情况是调用 sleep(long) 方法也会从 Runnable 状态进入 Timed Waiting 状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为 Runnable 状态
- 以下方法会让线程进入限期等待状态:
- 阻塞(Blocked):线程被阻塞了。
“阻塞状态” 与 “等待状态” 的区别是 “阻塞状态” 在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;
而 “等待状态” 则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。- 当获取锁失败后,由 可运行 进入 Monitor 的阻塞队列 Blocked,此时不占用 cpu 时间;
- 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的 Blocked 线程,唤醒后的线程进入 Runnable 状态
- 结束(Terminated):已终止线程的线程状态,线程已经结束执行。
- 线程内代码已经执行完毕,由可运行进入终结。此时会取消与底层线程关联
上述 6 种状态在遇到特定事件发生的时候将会互相转换,它们的转换关系如下图所示
1.7.操作系统层面的线程状态转换
从操作系统层面来看来线程状态则是五种:初始状态、可运行状态(就绪状态) 、运行状态、阻塞状态、终止状态。
- 运行态:分到 cpu 时间,能真正执行线程内代码的
- 就绪态:有资格分到 cpu 时间,但还未轮到它的
- 阻塞态:没资格分到 cpu 时间的
- 涵盖了 java 状态中提到的 阻塞、等待、有时限等待
- 多出了 阻塞 I/O,指线程在调用 阻塞 I/O 时,实际工作由 I/O 设备完成,此时线程无事可做,只能干等
- 新建与终结态:与 Java 线程中同名状态类似,不再赘述
- Java 线程中的 Runable 状态涵盖了操作系统层面的线程的 就绪、运行、阻塞 IO
2.共享问题
共享模型之管程-共享问题
2.1.Java 中的体现
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
多次运行上方的代码块,发现控制台输出结果中多次出现了不为 0 的数
2.2.问题分析
以上的结果可能是正数、负数、零。
这是因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
对于 i + + 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令
getstatic i // 获取静态变量 i 的值
iconst_1 // 准备常量 1
iadd // 自增
putstatic i // 将修改后的值存入静态变量 i
对于 i - - 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令
getstatic i // 获取静态变量 i 的值
iconst_1 // 准备常量 1
isub // 自减
putstatic i // 将修改后的值存入静态变量 i
Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题
但多线程下这 8 行代码可能交错运行
具体情况如下图所示(左边是结果为 负数 的情况;右边是结果为 正数 的情况)
简言之,造成上述结果中出现正负数的原因是:指令交错 和 线程上下文切换
2.3.临界区
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为 临界区
static int counter = 0;
static void increment() {
//临界区
counter++;
}
static void decrement() {
//临界区
counter--;
}
2.4.竞态条件
竞态条件(Race Condition)
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
3.synchronized
共享模型之管程:synchronized
3.1.基本概念
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized 即俗称的 对象锁
它采用互斥的方式让同一时刻至多只有一个线程能持有 对象锁,其它线程再想获取这个 对象锁 时就会阻塞住。
这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
- 注意
- 虽然 Java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized(对象) // 线程 1 持有对象锁时,线程 2 再想获取对象锁时 就会处于 blocked 状态
{
临界区
}
3.2.解决方案
加锁可解决之前的问题
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test4_3")
public class Test4_3 {
static int counter = 0;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
}
关于上述过程中 synchronized 关键字的图解
3.3.思考
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
为了加深理解,请思考下面的三个问题
- 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?
- 原子性
synchronized (lock) { for (int i = 0; i < 5000; i++) { counter--; } }
- 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?
- 无法保证正确性,因为这是在给两个不同的对象加锁。
- 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?
- t2 没有加 synchronized(obj),便不会去获取锁对象,这样便不会发生阻塞,依然无法保证正确性。
3.4.面向对象改进
关键字 synchorinized
- 指定加锁对象:对给定对象加锁,进入同步代码块前需要获得给定对象的锁。
把需要保护的共享变量放入一个类
输出结果
17:41:00 [main] c.Test4_3_2 - 0
3.5.方法上的 synchronized
关键字 synchorinized
- 直接作用于实例方法:相当于对当前实例加锁,进入同步代码块前需要获得当前实例的锁。
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码块前需要获得当前类的锁。
以下两处代码的效果等价
以下两处代码的效果等价
至于不加 synchorinized 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)
3.6.练习题:“线程八锁”
其实就是考察 synchornized 锁住的是哪个对象
- 代码 1:两者都是对实例方法加锁的,效果等同于对当前实例加锁,所以两者是互斥的关系。
输出结果可能为(先 1 后 2),也可能为(先 2 后 1)。 - 代码 2:大致同上,只是 a 方法中多加了一个 sleep() 方法。
输出结果可能为(2,1 秒后再输出 1),也可能为(1 秒后输出 1,之后输出 2)。
- 代码 3:因为 C 方法是不加锁的,所以是可以并行运行的。
不过打印顺序要考虑到 CPU 的调度问题,所以可能先 3 后 2,也可能先 2 后 3。
结果可能为(3 2,1 秒后输出 1)、(2 3,1 秒后输出 1)、(3,1 秒后输出 1 2)。 - 代码 4:因为 a、b 两方法都是对实例方法加锁,效果等同于给实例加锁。在主函数中,创建了 n1、n2 俩实例,所以两者可以并行运行。
结果为(2,1 秒后输出 1)
synchornized 关键字加在了 static 修饰的方法(即静态方法)上,效果等同于在当前类上加了锁;
synchornized 关键字加在了实例方法上,效果等同于在当前实例上加了锁;
这两者并不是同一对象。
- 代码 5:结果是 2,1 秒后出现 1
- 代码 6:结果是可能是 (2 ,1 秒后出现 1)、也可能是(1 秒后出现 1,2)
- 代码 7:结果是 2,1 秒后出现 1
- 代码 8:结果是可能是 (2 ,1 秒后出现 1)、也可能是(1 秒后出现 1,2)
4.线程安全分析
共享模型之管程:线程安全分析
4.1.成员变量和静态变量
成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
4.2.局部变量的线程安全分析
4.2.1. 局部变量
局部变量是否是线程安全的?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
public static void test1() {
int i = 10;
i++;
}
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
public static void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10
2: istore_0
3: iinc 0, 1
6: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I
但是,局部变量的引用则稍有不同
4.2.2. 局部变量引用
下面是一个成员变量的例子
/*
* method2() 和 method3() 中都调用了共享资源 list。
* 当多个线程执行,发生指令交错时,可能会出现指针问题
*/
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
method2();
method3();
}
}
private void method2() { list.add("1"); }
private void method3() { list.remove(0); }
}
多运行几次下面的代码,就可能会出现控制台输出报错信息的情况。
public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
控制台输出报错信息
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.remove(ArrayList.java:492)
at cn.itcast.n4.ThreadUnsafe.method3(TestThreadSafe.java:33)
at cn.itcast.n4.ThreadUnsafe.method1(TestThreadSafe.java:24)
at cn.itcast.n4.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14)
at java.lang.Thread.run(Thread.java:748)
- 分析
- 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
- method3 与 method2 分析相同
- 这里的内容对应的正好是视频的 第 65 P:线程安全分析-局部变量引用
- 我把这个课程的老师在视频下方的评论截图给贴过来了。
将 list 修改为局部变量后,就不存在上面的问题了
更改一下之前的主方法即可测试(ThreadSafe test = new ThreadSafe();
)
class ThreadSafe {
public void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) { list.add("1"); }
private void method3(ArrayList<String> list) { list.remove(0); }
}
- 分析
- list 是局部变量,每个线程调用时会创建其不同实例,没有共享
- 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
- method3 的参数分析与 method2 相同
换言之便是 没有共享,便没有伤害。(即 局部变量没有暴露给外部的时候是线程安全的)
但是当我们把这个局部变量的引用暴露给外部的时候,还会是线程安全的吗?
4.2.3. 暴露局部变量的引用给外部
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public,此时会不会出现线程安全问题?
-
情况 1:有其它线程调用 method2 和 method3
这种情况依旧没有线程安全问题。
虽然其他线程调用了 method2 和 method3方法,但它们传进去的参数(list 对象)肯定是不一样的 -
情况 2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法
class ThreadSafe { public void method1(int loopNumber) { ArrayList<String> list = new ArrayList<>(); for (int i = 0; i < loopNumber; i++) { method2(list); method3(list); } } public void method2(ArrayList<String> list) { list.add("1"); } public void method3(ArrayList<String> list) { list.remove(0); } }
class ThreadSafeSubClass extends ThreadSafe{ @Override public void method3(ArrayList<String> list) { new Thread(() -> { list.remove(0); }).start(); } }
这种情况会出现线程安全问题。
子类继承了父类,并重写了父类中的 method3()。在这个方法中,list 变量是被共享的。
其他线程实可以通过子类中重写的方法来访问和改变父类中的方法的局部变量的。
如果指令重排使得 list.add() 发生在 list.remove() 之前
重写是多态性的体现,这里涉及到了动态分派的问题。动态分派会调用invokevirtual
这条指令《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著 语
- 根据《Java 虚拟机规范》,
invokevirtual
指令的运行时(不包括特殊情况)解析过程大致分为以下几步:- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作 C。
- 如果在类型 C 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验。
如果通过权限校验,则返回这个方法的直接引用,查找过程结束;
如果不通过权限校验,则返回 java.lang.IllegalAccessError 异常。 - 否则,按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常。
- 因为
invokevirtual
指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual
指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是 Java 语言中方法重写的本质。 - 我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为 动态分派。
如果不想子类影响父类中的公共方法 method1() 的话,可以添加 final(即
public final void method1(int loopNumber) { }
) - 根据《Java 虚拟机规范》,
从这个例子可以看出 private 或 final 提供 安全 的意义所在,请体会开闭原则中的 闭。
4.3.常见的线程安全类
4.3.1.概述
String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent
包下的类
这里的内容主要来自于微信公众号 程序员 cxuan 中提供的 Java 基础核心总结.pdf
- String
- Integer
- StringBuffer
- StringBuffer 对象代表一个可变的字符串序列。
- 当一个 StringBuffer 被创建以后,通过 StringBuffer 的一系列方法可以实现字符串的拼接、截取等操作。
- 一旦通过 StringBuffer 生成了最终想要的字符串后,就可以调用其 toString 方法来生成一个新的字符串。
- + 操作符连接两个字符串,会自动执行 toString() 方法。但 toString() 方法不像 + —样自动被调用。
- StringBuffer 在字符串拼接上面直接使用 synchronized 关键字加锁,从而保证了线程安全性。
- Random
- Vector
- Vector 是 List 的实现类。与 ArrayList 一样,也是通过数组实现的。
- 不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性。
- 但 Vector 实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。
- Hashtable
- Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似。
- 不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable。
- 它的并发性是不如 ConcurrentHashMap 的,因为 ConcurrentHashMap 引入了分段锁。
- 不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。
- 一般不建议在新代码中使用 Hashtable 。
java.util.concurrent
包下的类- JDK 提供的大部分并发容器都在这个包中
- 比如:ConcurrentHashMap、CopyOnWirteArrayList、ConcurrentLinkedQueue、BlockingQueue、CoucurrentSkipListMap
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。
可以理解为如下代码块
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
- 注意:它们的每个方法是原子的,但它们多个方法的组合不是原子的。
4.3.2.线程安全类方法的组合
分析下面代码是否线程安全?
Hashtable table = new Hashtable();
// 线程 1,线程 2
if (table.get("key") == null) {
table.put("key", value);
}
线程 1 得到的数据为 null,之后释放锁。此后线程 2 得到的数据也为 null。两者都插入了数据,且其中必有一个数据被覆盖了。
所以上述的代码块不是线程安全的。
这里的问题就在于判断和动作不是一起的,if (table.get("key") == null)
这里的判断是没有加锁的。
4.3.3.不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
有同学或许有疑问,String 有 replace,substring 等方法可以改变值啊,那么这些方法又是如何保证线程安全的呢?
java/lang/String.java
源码
这里以 String 的 substring 方法为例(replace 方法的原理也和这个差不多)
public String substring(int beginIndex) {
if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); }
int subLen = value.length - beginIndex;
if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); }
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
创建新字符串的时候会在原有的 value 基础进行一个复制。复制完成之后,再赋值给新字符串的 value 属性。
public String(char value[], int offset, int count) {
if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); }
if (count <= 0) {
if (count < 0) { throw new StringIndexOutOfBoundsException(count); }
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); }
this.value = Arrays.copyOfRange(value, offset, offset + count);
}
不改动原有对象的属性,而是用新的对象来实现对象的不可变效果,这样保证了线程的安全性。(没有改变,也就没有线程安全问题)
4.4.实例分析
- 例 1
Servlet 是在 Tomcat 环境下运行的,只有一个实例,所以它肯定会被 Tomcat 的多个线程共享使用。
public class MyServlet_1 extends HttpServlet {
// 是否安全?(否。Hashtable 才是线程安全的)
Map<String, Object> map = new HashMap<>();
// 是否安全?(是,字符串属于不可变类)
String S1 = "...";
// 是否安全?(是)
final String S2 = "...";
// 是否安全?(否)
Date D1 = new Date();
// 是否安全?(否)
// final 修饰引用类型时,表示对其初始化之后便不能再让其指向另一个对象。
// 但是日期里面的其他属性(诸如年月日)仍然可以被改变。
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}
- 例 2
此处 Service 是 Servlet 的一个成员变量,而 Servlet 只有一个,所以这里 Sevice 也是会被多个线程共享的,其中的 count 也是共享资源。
public class MyServlet_2 extends HttpServlet {
// 是否安全?(否)
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++;
/* ****************[临界区]**************** */
}
}
- 例 3
下方的代码块是 Spring Aop 的一个典型应用(做一个 切面,加 前置通知 和 后置通知)
Spring 中每一个对象都是默认单例的(除非是加了 @Scope
注解)
既然是单例就意味着其需要被共享,此处对象中的成员变量也是需要被共享的
@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 和 end 都做成环绕通知中的局部变量,这样可以避免线程安全问题。
- 例 4
// 该类中没有成员变量,一般也就意味着(在多个线程访问的情况下)没有其他线程可以更改其的属性、状态
// 一般没有成员变量的类都是线程安全的
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全(是)
// 此处的 conn 属于方法内的局部变量
// 未来即使有多个线程同时访问它,也是互不干扰的。
// 例如:线程 1 创建 conn(1),线程 2 创建 conn(2),这两者都是互相独立的
try (Connection conn = DriverManager.getConnection("", "", "")) {
// ...
} catch (Exception e) {
// ...
}
}
}
public class UserServiceImpl implements UserService {
// 是否安全(是)
// 虽然该类中有成员变量 userDao,但 userDao 中没有可更改的属性(上面已经分析过了)
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class MyServlet_3 extends HttpServlet {
// 是否安全(是)
// 该类中有成员变量 userService,UerService 类中有 成员变量
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
- 例 5
public class UserDaoImpl implements UserDao {
// 是否安全(否)
// 此处的 conn 是成员变量
// 因为 servlet 实例只有一个,导致了 service 实例也只有一个,进而导致了 dao 实例也只有一个
// 此时 dao 中的成员变量自然是可以被多个线程被共享的
// 线程 1 刚刚创建 conn 之后,(线程 1 还没有来得及使用 conn)线程 2 就更改了 conn,这样一来就导致了线程安全问题
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("", "", "");
// ...
conn.close();
}
}
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class MyServlet_4 extends HttpServlet {
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
- 例 6
public class UserDaoImpl implements UserDao {
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("", "", "");
// ...
conn.close();
}
}
public class UserServiceImpl implements UserService {
public void update() {
// 这里是没有线程安全的问题的
// userDao 是作为 UserServieImpl 中的局部变量存在的
// 线程 1 调用它创建了一个 userDao,线程 2 调用它创建了一个新的 userDao,两者都不会互相干扰
// 但是不推荐这样写。
// 为了减少资源的占用、解决线程安全问题,还是应该把 connection 作为 Dao 中的局部变量来书写
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class MyServlet extends HttpServlet {
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
- 例 7
public abstract class Test {
public void bar() {
// 是否安全(否)
// sdf 是局部变量。但是我们还要判断这个 局部变量的引用 是否暴露给了外界
// 该类是一个抽象类。仍然有机会把该类中的局部变量传递给子类中的方法。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
// 子类中的方法就有可能被其他线程调用(比如直接在子类中创建一个新的线程),从而导致了多线程并发访问 sdf 的问题。
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
其中 foo 的行为是不确定的(子类重写了该方法),可能导致不安全的发生(多线程并发访问),被称之为外星方法
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();
}
}
即继承了父类的子类,子类中新创建了线程,这个线程和父类中的线程调用的对象是同一个,如此就会造成线程不安全问题
解决办法就是将父类子类中的 foo 方法设置为 private 或者 final
比较 JDK 中 String 类的实现
// String 类的修饰符就是 final,如此可阻止子类覆盖父类中的一些行为
public final class String{
// ... ...
}
4.6.卖票练习
测试下面代码是否存在线程安全问题,并尝试改正
// 售票窗口
public class TicketWindow {
private int count;
public TicketWindow(int count) { this.count = count; }
// 获取余票数量
public int getCount() { return count; }
// 售票
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}
@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
public static void main(String[] args) throws InterruptedException {
// 模拟多人买票
TicketWindow window = new TicketWindow(1000);
// 所有线程的集合
// 这里的变量不会被多个线程所共享,所以使用普通的 ArrayList 即可
List<Thread> threadList = new ArrayList<>();
// 卖出的票数统计
// Vector 是线程安全的 List 实现类,底层是通过数组实现的
List<Integer> amountList = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread thread = new Thread(() -> {
/* ****************[临界区]**************** */
// 买票
int amount = window.sell(random(5));
// 统计买票数
amountList.add(amount);
// Vector 的 add() 是加了锁 synchronized 的
// amount 和 amountList 是属于两个不同的共享变量,不需要考虑上面两个方法的组合的线程安全
/* ****************[临界区]**************** */
});
threadList.add(thread);
thread.start();
}
for (Thread thread : threadList) {
thread.join();
}
// 统计卖出的票数和剩余票数
log.debug("余票:{}", window.getCount());
log.debug("卖出的票数:{}", amountList.stream().mapToInt(i -> i).sum());
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~5
public static int random(int amount) {
return random.nextInt(amount) + 1;
}
}
首先要分析出临界区(多个线程对共享变量有读写操作的代码区)的代码位置。(已经在代码块中注释说明了)
显然,上述代码的临界区不是线程安全的。(可能会发生指令重排)
解决办法就是在 TicketWindow 类的 sell 方法上加上 synchronized 关键字。
public synchronized int sell(int amount){ }
4.7.转账练习
测试下面代码是否存在线程安全问题,并尝试改正
// 账户
public class Account {
private int money;
public Account(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
// 转账
public void transfer(Account target, int amount) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
@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 < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账 2000 次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}
启动主程序后发现控制台的打印结果并不是 2000(可能大于 2000,也可能小于 2000)
给 Account 类的 transfer 方法加上 synchronized
public void synchronized transfer(Account target, int amount) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
这样做是解决不了问题的。
将 synchronized 加在了成员方法上,即相当于把锁加在了当前类上(即 synchronized(this){ ... }
)
public void transfer(Account target, int amount) {
synchronized (this) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
此时只会保护自己的方法(this.setMoney() 和 this.getMoney()),但无法影响到另外一个对象上的方法
那么在此基础上再给 target 加锁呢?
这样确实可以解决当前问题,但是更容易产生死锁问题。
对于 this 和 target 来说,Account 类是它们所共享的,所以此处我们可以直接对 Account 加锁 synchronized (Account.class)
public void transfer(Account target, int amount) {
synchronized (Account.class) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
不过这样做的效果并不是很好(串行了,性能下降了),是有更好的方法的,这个之后再讲。
5.Monitor
共享模型之管程:Monitor
5.1.Java 对象头
参考资料:https://stackoverflow.com/questions/26357186/what-is-in-java-object-header
这里以 32 位虚拟机为例
普通对象
Object Header(64 bits) | |
---|---|
Mark Word(32 bits) | Klass Word(32 bits) |
数组对象
Object Header(96 bits) | ||
---|---|---|
Mark Word(32 bits) | Klass Word(32 bits) | array length(32 bits) |
其中 Mark Word 结构为
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
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 |
以下内容参考自 《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 (周志明 著)
HotSpot 虚拟机,是 Sun/OracleJDK 和 OpenJDK 中的默认 Java 虚拟机,也是目前使用范围最广的 Java 虚拟机。
该虚拟机中, 对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding)。
HotSpot 虚拟机对象的 对象头 部分包括两类信息:Mark Word 和 类型指针
第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32 个比特和 64 个比特,官方称它为 “Mark Word”。
对象需要存储的运行时数据很多,其实已经超出了 32、64 位 Bitmap 结构所能记录的最大限度,但对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word 被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间。
例如在 32 位的 HotSpot 虚拟机中,如对象未被同步锁锁定的状态下,Mark Word 的 32 个比特存储空间中的 25 个比特用于存储对象哈希码,4 个比特用于存储对象分代年龄,2 个比特用于存储锁标志位,1 个比特固定为 0,在其他状态(轻量级锁定、重量级锁定、GC 标记、可偏向)下对象的存储内容如下表所示。
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC 标记 |
偏向线程 ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针。
Java 虚拟机通过这个指针来确定该对象是哪个类的实例。
并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。
此外,如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。
接下来的 实例数据 部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle
参数)和字段在Java 源码中定义顺序的影响。
HotSpot 虚拟机默认的分配顺序为 longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
如果 HotSpot 虚拟机的 +XX:CompactFields
参数值为 true(默认就为 true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。
对象的第三部分是 对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
由于 HotSpot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是任何对象的大小都必须是 8 字节的整数倍。
对象头部分已经被设计成正好是 8 字节的倍数(1 倍或 2 倍),因此如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
5.2.Monitor 原理
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象
如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针
Monitor 的结构如下图所示
- 刚开始 Monitor 中 Owner 为 null
- 当 Thread-2 执行
synchronized(obj){ //临界区代码 }
就会将 Monitor 的所有者 Owner 置为 Thread-2
在 Monitor 内部,同一时刻只能有一个 Owner) - 在 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 的对象不会关联监视器,不遵从以上规则
5.3.Synchronized 原理
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
上方的代码块对应的字节码是(javap -v Java文件名称.class
)
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 引用 (synchronized 的开始)
3: dup # dup:复制操作数栈栈顶的内容,并将复制内容压入栈顶
# 此处是复制 lock 对象的引用,现在这里就有 lock 对象的两个引用了
# 分别对应着 monitorenter 和 monitorexit 两个指令使用的阶段
4: astore_1 # lock 引用 -> slot 1
# 这里将操作数栈中的 lock 引用压入局部变量表的 1 号槽位
# 如此这般是为了之后的解锁
5: monitorenter # 将 lock 对象的 Mark Word 置为 Monitor 指针
# 该指令会消耗掉栈顶元素(lock 对象的引用)
# 并对 lock 对象加锁
# #####################################################################################################
# 下面的这一段字节码就是 counter++ 的意思
6: getstatic #3 // Field counter:I
# <- i
9: iconst_1 # 准备常数 1
10: iadd # +1(即自增)
11: putstatic #3 // Field counter:I
# -> i
# #####################################################################################################
14: aload_1 # <- lock 引用
# 加载之前存储在 1 号槽位中的 lock 引用(astore_1)
15: monitorexit # 将 lock 对象 Mark Word 重置, 唤醒 EntryList
# 上锁了之后,Mark Word 中存储的就是 Monitor 的指针了
# 原先 Mark Word 中存储的内容则存储在了 Monitor 对象中
# 解锁重置会从 Montor 对象中取出数据,将之前 Mark Word 的内容还原
16: goto 24 # 这里的意思是直接跳到 24: return
# #####################################################################################################
# 以上都是同步代码块正常执行(正常解锁)的情况
# 下面的字节码(19 ~ 23)则是异常情况下,释放同步代码块的锁的操作
19: astore_2 # e -> slot 2
# 将异常对象的引用抛到局部变量表中的 2 号槽位上
20: aload_1 # <- lock 引用
# 加载之前暂存在 1 号槽位的对象的引用(astore_1)
21: monitorexit # 将 lock 对象 MarkWord 重置, 唤醒 EntryList
22: aload_2 # <- slot 2 (e)
# 加载暂存在 2 号槽位上的异常对象
23: athrow # throw e
# #####################################################################################################
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
LineNumberTable:
line 10: 0
line 11: 6
line 12: 14
line 13: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #4 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: putstatic #2 // Field lock:Ljava/lang/Object;
10: iconst_0
11: putstatic #3 // Field counter:I
14: return
LineNumberTable:
line 6: 0
line 7: 10
5.4.synchronized 优化
5.4.1.轻量级锁
使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized (obj) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized (obj) {
// 同步块 B
}
}
这里稍稍提一个概念:CAS(Compare and Swap),它体现的是一种乐观锁的思想
- 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
- 让锁记录中 Object reference 指向锁对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
- 如果 CAS 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
- 如果 CAS 失败,有两种情况
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,那么此时其会进行自旋锁。
自旋一定次数后,如果还是失败,那么它就进入锁膨胀阶段。(这个后面会讲) - 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
这里 Lock Record 不再和 object 进行值交换,Lock Record 存储的值是 null 而不是 Mark Word
可以通过查看 Lock Record 的数量来判断出一个线程对同一个对象加了几次锁
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,那么此时其会进行自旋锁。
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
- 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
5.4.2.锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功。
这时有一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),此时就需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized (obj) {
// 同步块
}
}
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址(10)
轻量级锁是没有阻塞这个概念的,要阻塞得申请 Monitor 锁 - 然后自己进入 Monitor 的 EntryList,变成 BLOCKED 状态
- 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址(10)
- 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头,失败。
这时会进入重量级解锁流程
即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
5.4.3.自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化。
如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
线程阻塞意味着线程需要发生上下文切换,上下文切换是非常耗性能的。
- 自旋重试成功的情况
线程 1 (cpu 1 上) | 对象 Mark | 线程 2 (cpu 2 上) |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取 monitor | 10(重量锁)重量锁指针 | - |
成功(加锁) | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | 访问同步块,获取 monitor |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行完毕 10 | (重量锁)重量锁指针 | 自旋重试 |
成功(解锁) | 01(无锁) | 自旋重试 |
- | 10(重量锁)重量锁指针 | 成功(加锁) |
- | 10(重量锁)重量锁指针 | 执行同步块 |
- | … | … |
- 自旋重试失败的情况
线程 1(cpu 1 上) | 对象 Mark | 线程 2(cpu 2 上) |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取 monitor | 10(重量锁)重量锁指针 | - |
成功(加锁) | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | 访问同步块,获取 monitor |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重量锁指针 | 阻塞 |
- | … | … |
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的。
比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;
反之,就少自旋甚至不自旋。总体上是比较智能的。 - Java 7 之后不能控制是否开启自旋功能
5.4.4.偏向锁1-简单介绍
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:
- 只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,
- 之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。
- 以后只要不发生竞争,这个对象就归该线程所有
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
}
}
- 轻量级锁 分析过程
- 偏向锁 分析过程
5.4.5.偏向锁2-偏向状态
先让我们回忆一下对象头格式(64 位)
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 |
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,Mark Word 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效。如果想避免延迟,可以加 VM 参数
-XX:BiasedLockingStartupDelay=0
来禁用延迟 - 如果没有开启偏向锁,那么对象创建后,Mark Word 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
-
测试延迟特性
-
测试偏向锁
class Dog {}
- 利用 jol 第三方工具来查看对象头信息(注意视频教程里扩展了 jol 让它输出更为简洁)
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10-TEST</version> </dependency>
// 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0 public static void main(String[] args) throws IOException { Dog d = new Dog(); ClassLayout classLayout = ClassLayout.parseInstance(d); new Thread(() -> { log.debug("synchronized 前"); System.out.println(classLayout.toPrintableSimple(true)); synchronized (d) { log.debug("synchronized 中"); System.out.println(classLayout.toPrintableSimple(true)); } log.debug("synchronized 后"); System.out.println(classLayout.toPrintableSimple(true)); }, "t1").start(); }
- 输出
11:08:58.117 c.TestBiased [t1] - synchronized 前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 11:08:58.121 c.TestBiased [t1] - synchronized 中 00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 11:08:58.121 c.TestBiased [t1] - synchronized 后 00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
- 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
- 另外,此处的线程 id 是操作系统指定的,与 Java 中的线程对象不是一一对应的
-
测试禁用
- 在上面测试代码运行时在添加 VM 参数
-XX:-UseBiasedLocking
禁用偏向锁,控制台输出如下
11:13:10.018 c.TestBiased [t1] - synchronized 前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 11:13:10.021 c.TestBiased [t1] - synchronized 中 00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000 11:13:10.021 c.TestBiased [t1] - synchronized 后 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
禁用了偏向锁之后,其直接应用了轻量级锁。如果有竞争发生,轻量级锁则会膨胀为重量级锁。
所以得出加锁顺序的优先级:偏向锁 > 轻量级锁 > 重量级锁 - 在上面测试代码运行时在添加 VM 参数
5.4.6.偏向锁3-撤销1-调用对象 hashCode
- 注意:这里的哈希码指的是一致性的哈希码:Object::hashCode(),不包括自己重写的哈希码
调用了对象的 hashCode,但偏向锁的对象 Mark Word 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
当对象处于轻量级锁状态(Biased,101)时,Mark Word 中已经存不下 hashCode 了。
- Mark Word 是 64 位,hashCode 是 31 位
- 偏向锁状态下的 Mark Word 里,线程 ID 就已经占了 54 位了,完全没有多余的位置。
当一个可偏向的对象调用了 hashCode() 方法后,它就会撤销这个对象的偏向状态,变为 Normal 状态(001)。
- 轻量级锁会在线程栈帧中的锁记录中记录 hashCode,解锁时使用 CAS 将 Mark Word 的值恢复给对象头
- 重量级锁会在 Monitor 对象中记录 hashCode,将来解锁时还会将其还原(交换)
-
测试 hashCode
- 在调用 hashCode 后需要使用偏向锁,要记得去掉之前加的
-XX:-UseBiasedLocking
- 正常状态对象一开始是没有 hashCode 的,第一次调用才生成,填充到 Mark Word 中
d.hashCode(); // 会禁用这个对象的偏向锁
- 在调用 hashCode 后需要使用偏向锁,要记得去掉之前加的
-
输出
11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015 11:22:10.391 c.TestBiased [t1] - synchronized 前 00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001 11:22:10.393 c.TestBiased [t1] - synchronized 中 00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000 11:22:10.393 c.TestBiased [t1] - synchronized 后 00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
5.4.7.偏向锁4-撤销2-其它线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
想要达到上面的效果,需要注意:错开两个线程使用锁对象的时间。
如果这两个线程直接来竞争锁,那么很容易发生锁升级为重量级锁的情况。(这个在锁膨胀里已经讲过了)
private static void test2() throws InterruptedException {
Dog d = new Dog();
Thread t1 = new Thread(() -> {
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
synchronized (TestBiased.class) {
TestBiased.class.notify();
}
// 如果不用 wait/notify 使用 join 必须打开下面的注释
// 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的
/*try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}*/
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (TestBiased.class) {
try {
TestBiased.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}, "t2");
t2.start();
}
- 输出
[t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
[t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
[t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000
[t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
5.4.8.偏向锁5-撤销3-调用 wait / notify
调用 wait/notify 会导致偏向状态被撤销
调用 wait 方法和 notify 方法会导致锁膨胀为重量级锁
public static void main(String[] args) throws InterruptedException {
Dog d = new Dog();
Thread t1 = new Thread(() -> {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
try {
d.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t1");
t1.start();
new Thread(() -> {
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (d) {
log.debug("notify");
d.notify();
}
}, "t2").start();
}
- 输出
[t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
[t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101
[t2] - notify
[t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010
5.4.9.偏向锁6-批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
当撤销偏向锁阈值超过 20 次后,JVM 会这样觉得,我是不是偏向错了呢?
于是会在给这些对象加锁时重新偏向至加锁线程
private static void test3() throws InterruptedException {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
synchronized (list) {
list.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("===============> ");
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t2");
t2.start();
}
- 控制台输出
[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - ===============> # 此处开始撤销偏向锁的操作
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 # 偏向锁
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 # 此处升级为了轻量级锁
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 # 不可偏向的正常状态
[t2] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 1 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 2 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 3 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 4 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 5 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 6 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 7 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 7 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 8 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 9 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 9 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 10 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 10 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 11 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 11 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 12 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 12 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 13 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 13 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 14 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 14 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 15 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 15 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 16 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 16 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 17 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 17 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 # 此处不再撤销偏向锁了
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 # 此处开始偏向了另一个线程
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
5.4.9.偏向锁7-批量撤销
当撤销偏向锁阈值超过 40 次后,JVM 会这样觉得,自己确实偏向错了,根本就不该偏向。
于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
static Thread t1, t2, t3;
private static void test4() throws InterruptedException {
Vector<Dog> list = new Vector<>();
int loopNumber = 39;
t1 = new Thread(() -> {
for (int i = 0; i < loopNumber; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
LockSupport.unpark(t2);
}, "t1");
t1.start();
t2 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
LockSupport.unpark(t3);
}, "t2");
t2.start();
t3 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t3");
t3.start();
t3.join();
log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
}
打印信息比较多,此处就不粘贴了。
「偏向锁」内容的参考资料
- https://github.com/farmerjohngit/myblog/issues/12
- https://www.cnblogs.com/LemonFive/p/11246086.html
- https://www.cnblogs.com/LemonFive/p/11248248.html
5.5.锁消除
以下内容参考自 《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 (周志明 著)
锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码要求同步,但是会对被检测到的 不可能存在共享数据竞争的锁 进行消除。
锁消除的主要判定依据来源于逃逸分析的数据支持。
如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。
逃逸分析(Escape Analysis)是目前 Java 虚拟机中比较前沿的优化技术,它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。
逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用(例如作为调用参数传递到其他方法中),这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。
如果能证明一个对象不会逃逸到方法或线程之外(换句话说是别的方法或线程无法通过任何途径访问到这个对象),或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化。
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
static int x = 0;
@Benchmark
public void a() throws Exception {
x++;
}
@Benchmark
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++;
}
}
}
java -jar benchmarks.jar
Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.542 0.056 ns/op
c.i.MyBenchmark.b avgt 5 1.518 0.091 ns/op
java -XX:-EliminateLocks -jar benchmarks.jar
(添加了 关闭锁消除优化 的参数)
Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.507 0.108 ns/op
c.i.MyBenchmark.b avgt 5 16.976 1.572 ns/op
5.6.锁粗化
锁粗化:对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。
以下内容参考自 《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 (周志明 著)
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等待锁的线程也能尽可能快地拿到锁。
大多数情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
6.wait/notify
共享模型之管程:wait / notify
6.1.wait/notify 的原理
-
Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
-
BLOCKED 和 WAITING 的线程都处于阻塞态(操作系统层面),不占用 CPU 时间片
-
BLOCKED 线程会在 Owner 线程释放锁时唤醒
-
WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
-
注意
BLOCKED 状态的线程是正在等待锁的释放的线程
而 WAITING 状态的线程是已经成功获得过锁,因为条件不满足,所以放弃了锁的线程
6.2.wait/notify 的 api
public final void wait() throws InterruptedExceptionpublic
public final native void notify()
obj.wait()
让进入 object 监视器的线程到 waitSet 等待obj.notify()
在 object 上正在 waitSet 等待的线程中挑一个唤醒obj.notifyAll()
让 object 上正在 waitSet 等待的线程全部唤醒wait()
方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止wait(long n)
方法是有时限的等待,到 n 毫秒后结束等待,或是被 notify
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
@Slf4j(topic = "c.Test18")
public class Test18 {
static final Object lock = new Object();
public static void main(String[] args) {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
如果不用 synchronized (lock)
获得对象的锁的话,是无法调用 wait() 和 notify() 的,会在控制台输出如下报错信息
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at thread.Test1.main(Test1.java:18)
可以用下面的代码块来观察 notify() 和 notifyAll() 的区别
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在 obj 上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}, "t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在 obj 上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}, "t2").start();
// 主线程两秒后执行
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
// obj.notify(); // 唤醒 obj 上一个线程
obj.notifyAll(); // 唤醒 obj 上所有等待线程
}
}
}
调用 notify() 的输出结果
20:00:53.096 [Thread-0] c.TestWaitNotify - 执行....
20:00:53.099 [Thread-1] c.TestWaitNotify - 执行....
20:00:55.096 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
20:00:55.096 [Thread-0] c.TestWaitNotify - 其它代码....
调用 notifyAll() 的输出结果
19:58:15.457 [Thread-0] c.TestWaitNotify - 执行....
19:58:15.460 [Thread-1] c.TestWaitNotify - 执行....
19:58:17.456 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代码....
19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代码....
6.3.wait/notify 的正确使用
sleep(long n) 和 wait(long n)
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
- sleep(long n) 和 wait(long n) 的状态都是 TIMED_WAITING
注意:不带参数的 wait() 进入的是 WAITING 状态
@Slf4j(topic = "c.Test19")
public class Test19 {
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock) {
log.debug("获得锁");
try {
// Thread.sleep(20000);
lock.wait(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
Sleeper.sleep(1);
synchronized (lock) {
log.debug("获得锁");
}
}
}
如果使用的是 Thread.sleep(20000);
,则控制台输出如下内容(sleep(long n) 时间结束后,线程才获得了锁)
10:16:56.682 c.Test19 [t1] - 获得锁
10:17:16.697 c.Test19 [main] - 获得锁
如果使用的是 lock.wait(20000);
,则控制台输出如下内容(因为 Sleeper.sleep(1) ,所以 1 秒后才获得了锁)
10:17:42.555 c.Test19 [t1] - 获得锁
10:17:43.559 c.Test19 [main] - 获得锁
究其上述情况的原因便是:sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
6.4.举例说明
这是下面 5 处代码块中的成员变量,都是一样的
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
6.4.1.step_1
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
sleep(2);
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
}
}, "送烟的").start();
}
10:50:02.240 c.TestCorrectPosture [小南] - 有烟没?[false]
10:50:02.244 c.TestCorrectPosture [小南] - 没烟,先歇会!
10:50:04.250 c.TestCorrectPosture [小南] - 有烟没?[false]
10:50:04.250 c.TestCorrectPosture [送烟的] - 烟到了噢!
10:50:04.250 c.TestCorrectPosture [其它人] - 可以开始干活了
10:50:04.250 c.TestCorrectPosture [其它人] - 可以开始干活了
10:50:04.250 c.TestCorrectPosture [其它人] - 可以开始干活了
10:50:04.250 c.TestCorrectPosture [其它人] - 可以开始干活了
10:50:04.251 c.TestCorrectPosture [其它人] - 可以开始干活了
- 其它干活的线程,都要一直阻塞,效率太低
- 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
- 加了
synchronized (room)
后,就好比小南在里面反锁了门睡觉,烟根本没法送进门
main 没加 synchronized 就好像 main 线程是翻窗户进来的 - 解决方法,使用 wait - notify 机制
6.4.2.step_2
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
room.notify();
}
}, "送烟的").start();
}
10:56:50.686 c.TestCorrectPosture [小南] - 有烟没?[false]
10:56:50.690 c.TestCorrectPosture [小南] - 没烟,先歇会!
10:56:50.690 c.TestCorrectPosture [其它人] - 可以开始干活了
10:56:50.690 c.TestCorrectPosture [其它人] - 可以开始干活了
10:56:50.690 c.TestCorrectPosture [其它人] - 可以开始干活了
10:56:50.690 c.TestCorrectPosture [其它人] - 可以开始干活了
10:56:50.690 c.TestCorrectPosture [其它人] - 可以开始干活了
10:56:51.687 c.TestCorrectPosture [送烟的] - 烟到了噢!
10:56:51.687 c.TestCorrectPosture [小南] - 有烟没?[true]
10:56:51.687 c.TestCorrectPosture [小南] - 可以开始干活了
- 解决了其它干活的线程阻塞的问题
- 但如果有其它线程也在等待条件呢?
6.4.3.step_3
// 虚假唤醒
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
10:59:59.532 c.TestCorrectPosture [小南] - 有烟没?[false]
10:59:59.534 c.TestCorrectPosture [小南] - 没烟,先歇会!
10:59:59.534 c.TestCorrectPosture [小女] - 外卖送到没?[false]
10:59:59.534 c.TestCorrectPosture [小女] - 没外卖,先歇会!
11:00:00.533 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
11:00:00.533 c.TestCorrectPosture [小女] - 外卖送到没?[true]
11:00:00.533 c.TestCorrectPosture [小女] - 可以开始干活了
11:00:00.533 c.TestCorrectPosture [小南] - 有烟没?[false]
11:00:00.533 c.TestCorrectPosture [小南] - 没干成活...
- notify 只能随机唤醒一个 WaitSet 中的线程
这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,这种情况称之为 虚假唤醒 - 解决方法,改为 notifyAll
6.4.4.step_4
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
11:36:25.431 c.TestCorrectPosture [小南] - 有烟没?[false]
11:36:25.435 c.TestCorrectPosture [小南] - 没烟,先歇会!
11:36:25.435 c.TestCorrectPosture [小女] - 外卖送到没?[false]
11:36:25.435 c.TestCorrectPosture [小女] - 没外卖,先歇会!
11:36:26.432 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
11:36:26.432 c.TestCorrectPosture [小女] - 外卖送到没?[true]
11:36:26.432 c.TestCorrectPosture [小女] - 可以开始干活了
11:36:26.433 c.TestCorrectPosture [小南] - 没烟,先歇会!
11:36:26.433 c.TestCorrectPosture [小南] - 没干成活...
- 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了
- 解决方法,用 while + wait,当条件不成立,再次 wait
6.4.5.step_5
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
11:40:46.261 c.TestCorrectPosture [小南] - 有烟没?[false]
11:40:46.266 c.TestCorrectPosture [小南] - 没烟,先歇会!
11:40:46.266 c.TestCorrectPosture [小女] - 外卖送到没?[false]
11:40:46.266 c.TestCorrectPosture [小女] - 没外卖,先歇会!
11:40:47.271 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
11:40:47.271 c.TestCorrectPosture [小女] - 外卖送到没?[true]
11:40:47.271 c.TestCorrectPosture [小女] - 可以开始干活了
11:40:47.271 c.TestCorrectPosture [小南] - 没烟,先歇会!
6.4.6.总结
正确使用 wait 和 notify 的姿势
synchronized (lock) {
while (条件不成立) { // 一直重试,当条件不成立时,继续判断
lock.wait();
}
// 干活
}
//另一个线程
synchronized (lock) {
// 唤醒所有等待线程,避免 虚假唤醒 的情况发生
lock.notifyAll();
}
7.设计模式-1
共享模型之管程:设计模式-1
7.1.保护性暂停
7.1.1.原理
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
7.1.2.基本实现
GuardedObject.java
public class GuardedObject_1 {
// 结果
private Object response;
// 获取结果
public Object get() {
synchronized (this) {
// 条件不满足时,继续等待
while (response == null) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
// 唤醒所有等待线程
this.notifyAll();
}
}
}
测试
ProtectivePause_Test1.java
@Slf4j(topic = "c.ProtectivePause_Test1")
public class ProtectivePause_Test1 {
// [线程 1] 等待 [线程 2] 的下载结果
public static void main(String[] args) {
GuardedObject_1 guardedObject1 = new GuardedObject_1();
new Thread(() -> {
log.debug("等待结果");
List<String> list = (List<String>) guardedObject1.get();
log.debug("结果大小:{}", list.size());
}, "t1").start();
new Thread(() -> {
log.debug("执行下载");
try {
List<String> list = Downloader.download();
guardedObject1.complete(list);
} catch (IOException e) {
e.printStackTrace();
}
}, "t2").start();
}
}
输出结果
11:57:55.242 [t2] c.ProtectivePause_Test1 - 执行下载
11:57:55.242 [t1] c.ProtectivePause_Test1 - 等待结果
11:57:56.521 [t1] c.ProtectivePause_Test1 - 结果大小:3
这个设计模式其实就是两个线程之间交互结果的模式
使用 join() 可以交互结果,但是有俩缺点:
- 一个线程工作必须等待另一个线程运行结束;
例:t1 线程运行到 t2.join() 这行代码时,暂停 t1 线程,一直等到 t2 线程运行完毕后,t1 线程才可以继续执行其他的事情 - 等待结果的变量必须设置为全局变量
保护性暂停设计模式只需要一个消息通知,通知了就可以继续工作;等待结果的变量可以设置为局部变量
这种模式适用于 线程中的一部分代码需要同步,而后续代码块不需要同步 的情况。
7.1.3.增加超时
public class GuardedObject_2 {
// 结果
private Object response;
// 获取结果
// timeout:最大等待时间,表示需要等待多久
public Object get(long timeout) {
synchronized (this) {
// 记录开始等待时间
long beginTime = System.currentTimeMillis();
// 记录经历的时间
long passedTime = 0;
// 条件不满足时,继续等待
while (response == null) {
// waitTime:这一轮循环需要等待的时间
// 这样做的目的是为了避免虚假唤醒造成的等待时间变长的问题
long waitTime = timeout - passedTime;
// 经历时间 超过 最大等待时间,退出循环
if (waitTime <= 0) { break; }
try {
// 存在[虚假唤醒,没有结果返回]的情况,这个时候不应该继续等待 timeout 时长
// 举个例子,比如说 15:01 唤醒了线程,但没有结果返回
// 因为返回结果为空,故再次进入循环,但此时不应该是继续等待 2 秒,而是应该等待 1 秒了。
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
passedTime = System.currentTimeMillis() - beginTime; // 刷新等待过程的经历时间
}
return response;
}
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
// 唤醒所有等待线程
this.notifyAll();
}
}
}
测试
@Slf4j(topic = "c.ProtectivePause_Test2")
public class ProtectivePause_Test2 {
// [线程 1] 等待 [线程 2] 的下载结果
public static void main(String[] args) {
GuardedObject_2 guardedObject2 = new GuardedObject_2();
new Thread(() -> {
log.debug("t1 线程开始");
Object response = guardedObject2.get(2000);
log.debug("结果是:{}", response);
}, "t1").start();
new Thread(() -> {
log.debug("t2 线程开始");
Sleeper.sleep(1);
guardedObject2.complete(new Object());
}, "t2").start();
}
}
输出结果
11:58:20.888 [t1] c.ProtectivePause_2 - t1 线程开始
11:58:20.888 [t2] c.ProtectivePause_2 - t2 线程开始
11:58:21.901 [t1] c.ProtectivePause_2 - 结果是:java.lang.Object@65c12e7e
更改一下 ProtectivePause_Test2.java 中的代码:Sleeper.sleep(3);
即模拟超时效果(最大等待时间内,没有来得及返回结果)
13:07:45.855 [t2] c.ProtectivePause_Test2 - t2 线程开始
13:07:45.855 [t1] c.ProtectivePause_Test2 - t1 线程开始
13:07:47.866 [t1] c.ProtectivePause_Test2 - 结果是:null
更改一下 ProtectivePause_Test2.java 中的代码:guardedObject2.complete(null);
即模拟虚假唤醒的效果
13:08:17.008 [t1] c.ProtectivePause_Test2 - t1 线程开始
13:08:17.008 [t2] c.ProtectivePause_Test2 - t2 线程开始
13:08:19.023 [t1] c.ProtectivePause_Test2 - 结果是:null
7.2.join 原理
保护性暂停是一个线程等待另一个线程的结果,join() 是一个线程等待另一个线程的结束。
两者的实现方式都是一样的。
java/lang/Thread.java
public final void join() throws InterruptedException {
join(0);
}
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; // delay:这一轮循环需要的等待时间
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base; // 刷新等待的经历时间
}
}
}
join() 方法的本质是在当前线程对象实例上调用线程的 wait() 方法
7.3.解耦等待和生产
7.3.1.原理
图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员。如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类, 这样不仅能够解耦 结果等待者 和 结果生产者,还能够同时支持多个任务的管理。
7.3.2.实现
GuardedObject_3.java
public class GuardedObject_3 {
// GuardedObject 的唯一标识
private int id;
public GuardedObject_3(int id) {
this.id = id;
}
public int getId() {
return id;
}
// 结果
private Object response;
// 获取结果
// 此处的 timeout 表示需要等待多久
public Object get(long timeout) {
synchronized (this) {
// 记录开始等待时间
long beginTime = System.currentTimeMillis();
// 记录经历的时间
long passedTime = 0;
// 条件不满足时,继续等待
while (response == null) {
// 这一轮循环需要等待的时间
long waitTime = timeout - passedTime;
// 经历时间 超过 最大等待时间,退出循环
if (waitTime <= 0) {
break;
}
try {
// 存在 虚假唤醒,没有结果返回 的情况
// 这个时候不应该继续等待 timeout 时长
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
passedTime = System.currentTimeMillis() - beginTime;
}
return response;
}
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
// 唤醒所有等待线程
this.notifyAll();
}
}
}
Mailboxes.java
public class Mailboxes {
private static Map<Integer, GuardedObject_3> boxes = new Hashtable<>();
private static int id = 1;
// 产生唯一性的 id
private static synchronized int generateId() {
return id++;
}
public static GuardedObject_3 createGuardedObject() {
GuardedObject_3 go = new GuardedObject_3(generateId());
boxes.put(go.getId(), go);
return go;
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
public static GuardedObject_3 getGuardedObject(int id) {
return boxes.remove(id); // 返回 id 并删除该 id
}
}
People.java
@Slf4j(topic = "c.People")
public class People extends Thread {
@Override
public void run() {
// 收信
GuardedObject_3 guardedObject = Mailboxes.createGuardedObject();
log.debug("开始收信 id:{}", guardedObject.getId());
Object mail = guardedObject.get(5000);
log.debug("收到信 id:{}, 内容:{}", guardedObject.getId(), mail);
}
}
Postman.java
@Slf4j(topic = "c.Postman")
public class Postman extends Thread {
private int id;
private String mail;
public Postman(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
GuardedObject_3 guardedObject = Mailboxes.getGuardedObject(id);
log.debug("送信 id:{}, 内容:{}", id, mail);
guardedObject.complete(mail);
}
}
7.3.3.测试
ProtectivePause_Test3.java
@Slf4j(topic = "c.ProtectivePause_Test3")
public class ProtectivePause_Test3 {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new People().start();
}
Sleeper.sleep(1);
for (Integer id : Mailboxes.getIds()) {
new Postman(id, "内容" + id).start();
}
}
}
控制台输出
16:57:00.170 [Thread-2] c.People - 开始收信 id:2
16:57:00.170 [Thread-0] c.People - 开始收信 id:1
16:57:00.170 [Thread-1] c.People - 开始收信 id:3
16:57:01.179 [Thread-4] c.Postman - 送信 id:2, 内容:内容2
16:57:01.179 [Thread-3] c.Postman - 送信 id:3, 内容:内容3
16:57:01.180 [Thread-2] c.People - 收到信 id:2, 内容:内容2
16:57:01.180 [Thread-1] c.People - 收到信 id:3, 内容:内容3
16:57:01.180 [Thread-5] c.Postman - 送信 id:1, 内容:内容1
16:57:01.180 [Thread-0] c.People - 收到信 id:1, 内容:内容1
基本上可以理解为点对点投递,一一对应的模式。
7.4.生产者/消费者·模式
7.4.1.定义
- 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK 中各种阻塞队列,采用的就是这种模式
7.4.2.实现
线程之间通讯,一般是需要用到 id 的。可以于此创建一个用来存储 id 的封装类。
Message.java
final class Message {
private int id;
private Object value;
public Message(int id, Object value) {
this.id = id;
this.value = value;
}
public int getId() {
return id;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
消息队列类,用于 Java 消息队列类。
MessageQueue.java
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
// 消息的队列集合
private LinkedList<Message> list = new LinkedList<>();
// 队列容量
private int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
}
// 获取消息
public Message take() {
// 检查队列是否为空
synchronized (list) {
while (list.isEmpty()) {
try {
log.debug("队列为空,消费者线程需要等待 ... ");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 从队列的头部获取消息并且返回(且删除消息)
Message message = list.removeFirst();
log.debug("已消费消息 {}", message);
list.notifyAll();
return message;
}
}
// 存入消息
public void put(Message message) {
synchronized (list) {
// 检查对象是否已经满了
while (list.size() == capacity) {
try {
log.debug("队列已满,生产者线程需要等待 ... ");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 将消息加入队列的尾部
list.addLast(message);
log.debug("已生产消息 {}", message);
list.notifyAll();
}
}
}
7.4.3.测试
测试类:TestProCon.java
@Slf4j(topic = "c.TestProCon")
public class TestProCon {
public static void main(String[] args) {
MessageQueue messageQueue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
messageQueue.put(new Message(id, "值" + id));
}, "生产者" + i).start();
}
new Thread(() -> {
while (true) {
Sleeper.sleep(1);
Message message = messageQueue.take();
}
}, "消费者").start();
}
}
控制台输出结果
16:29:32.607 [生产者0] c.MessageQueue - 已生产消息 Message{id=0, value=值0}
16:29:32.610 [生产者1] c.MessageQueue - 已生产消息 Message{id=1, value=值1}
16:29:32.610 [生产者2] c.MessageQueue - 队列已满,生产者线程需要等待 ...
16:29:33.611 [消费者] c.MessageQueue - 已消费消息 Message{id=0, value=值0}
16:29:33.611 [生产者2] c.MessageQueue - 已生产消息 Message{id=2, value=值2}
16:29:34.622 [消费者] c.MessageQueue - 已消费消息 Message{id=1, value=值1}
16:29:35.635 [消费者] c.MessageQueue - 已消费消息 Message{id=2, value=值2}
16:29:36.645 [消费者] c.MessageQueue - 队列为空,消费者线程需要等待 ...
8.Park/Unpark
共享模型之管程:Park / Unpark
8.1.基本使用
Park、Unpark 都是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
8.2.特点
与 Object 的 wait & notify 相比 wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
park & unpark 是以线程为单位来 阻塞 和 唤醒 线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么 精确
park & unpark 可以先 unpark,而 wait & notify 不能先 notify
8.3.原理
每个线程都有自己的一个 Parker 对象(Java 中不可见),由三部分组成 _counter
,_cond
和 _mutex
打个比喻,线程就像一个旅人
- Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。
_counter
就好比背包中 的备用干粮(0 为耗尽,1 为充足) - 调用 park 就是要看需不需要停下来歇息
- 如果备用干粮耗尽,那么钻进帐篷歇息
- 如果备用干粮充足,那么不需停留,继续前进
- 调用 unpark,就好比令干粮充足
- 如果这时线程还在帐篷,就唤醒让他继续前进
- 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
- 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮
- 当前线程调用 Unsafe.park() 方法
- 检查
_counter
,本情况为 0,这时,获得_mutex
互斥锁 - 线程进入
_cond
条件变量阻塞 - 设置 _counter = 0
- 调用 Unsafe.unpark(Thread_0) 方法,设置
_counter
为 1 - 唤醒
_cond
条件变量中的 Thread_0 - Thread_0 恢复运行
- 设置
_counter
为 0
- 调用 Unsafe.unpark(Thread_0) 方法,设置
_counter
为 1 - 当前线程调用 Unsafe.park() 方法
- 检查
_counter
,本情况为 1,这时线程无需阻塞,继续运行 - 设置
_counter
为 0
9.线程状态转换
共享模型之管程:重新理解线程状态的转换
吐槽:其实这段我在第一章节也写过一些内容。但这个确实是重点,视频教程里也有,我也就照抄下来了。
可以回顾一下本博客中的第一章节的部分复习内容:1.6.Java 线程的状态转换
- NEW:线程刚被创建,但是还没有调用
start()
方法 - RUNNABLE:调用了
start()
方法之后- 注意:Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 可运行状态、运行状态 和 阻塞状态
- 由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行
- BLOCKED , WAITING, TIMED_WAITING 都是 Java API 层面对 阻塞状态 的细分,后面会在状态转换一节详述
- TERMINATED 当线程代码运行结束
假设有线程 Thread t
9.1.NEW --> RUNNABLE
New 状态表示创建了一个 Java 的线程对象,但是还没有和操作系统的线程关联起来。
当调用 t.start()
方法时,NEW --> RUNNABLE,此时线程对象就和操作系统底层的线程关联起来了。
9.2.RUNNABLE <–> WAITING
9.2.1.情况 1
t 线程用 synchronized(obj)
获取了对象锁后
- 调用
obj.wait()
方法时,t 线程从 RUNNABLE --> WAITING - 调用
obj.notify()
、obj.notifyAll()
、t.interrupt()
时- 竞争锁成功,t 线程从 WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 WAITING --> BLOCKED
9.2.2.情况 2
- 当前线程调用
t.join()
方法时,当前线程从 RUNNABLE --> WAITING- 注意是当前线程在 t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的
interrupt()
时,当前线程从 WAITING --> RUNNABLE
9.2.3.情况 3
-
当前线程调用
LockSupport.park()
方法会让当前线程从 Runnable --> WAITING -
调用
LockSupport.unpark(目标线程)
或调用了线程 的interrupt()
,会让目标线程从 WAITING --> Runnable
9.3.RUNNABLE <–> TIMED_WAITING
9.3.1.情况 1
t 线程用 synchronized(obj)
获取了对象锁后
- 调用
obj.wait(long n)
方法时,t 线程从 RUNNABLE --> TIMED_WAITING - 调用
obj.notify()
、obj.notifyAll()
、t.interrupt()
时- 竞争锁成功,t 线程从 WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 WAITING --> BLOCKED
9.3.2.情况 2
- 当前线程调用
t.join(long n)
方法时,当前线程从 RUNNABLE --> TIMED_WAITING- 注意是当前线程在 t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的
interrupt()
时,当前线程从 TIMED_WAITING --> RUNNABLE
9.3.3.情况 3
- 当前线程调用
Thread.sleep(long n)
,当前线程从 RUNNABLE --> TIMED_WAITING - 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
9.3.4.情况 4
- 当前线程调用
LockSupport.parkNanos(long nanos)
或LockSupport.parkUntil(long millis)
时,
当前线程从 RUNNABLE --> TIMED_WAITING - 调用
LockSupport.unpark(目标线程)
或调用了线程 的interrupt()
,或是等待超时,
会让目标线程从 TIMED_WAITING --> RUNNABLE
9.4.RUNNABLE <–> BLOCKED
-
t 线程用
synchronized(obj)
获取了对象锁时如果竞争失败,从 Runnable --> BLOCKED -
持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,
如果其中 t 线程竞争 成功,从 BLOCKED --> Runnable,其它失败的线程仍然 BLOCKED
9.5.RUNNABLE <–> TERMINATED
- 当前线程所有代码运行完毕,进入 TERMINATED
10.活跃性
共享模型之管程:活跃性
10.1.多把锁
使用多把锁可以将锁的粒度细分
- 好处:可以增强并发度
- 坏处:如果一个线程需要同时获得多把锁,就容易发生死锁
举例
一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小雨要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低。
解决方法是准备多个房间(多个对象锁)
Room.java
@Slf4j(topic = "c.Room")
public class Room {
public void sleep() {
synchronized (this) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (this) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}
TestMultiLock.java
public class TestMultiLock {
public static void main(String[] args) {
Room room = new Room();
new Thread(() -> {
room.study();
}, "小南").start();
new Thread(() -> {
room.sleep();
}, "小雨").start();
}
}
控制台输出(显然这里的并发度太低了,小雨必须要等小南完学习完才可以睡觉)
18:44:36.712 [小南] c.Room - study 1 小时
18:44:37.720 [小雨] c.Room - sleeping 2 小时
此时我们可以改进一下代码(设置多个对象锁)来增强并发度
BigRoom.java
@Slf4j(topic = "c.BigRoom")
class BigRoom {
private final Object studyRoom = new Object();
private final Object bedRoom = new Object();
public void sleep() {
synchronized (bedRoom) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (studyRoom) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}
TestMultiLock.java
public class TestMultiLock {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
Room room = new Room();
new Thread(() -> {
//room.study();
bigRoom.study();
}, "小南").start();
new Thread(() -> {
//room.sleep();
bigRoom.sleep();
}, "小雨").start();
}
}
控制台输出
18:52:45.909 [小雨] c.BigRoom - sleeping 2 小时
18:52:45.909 [小南] c.BigRoom - study 1 小时
10.2.死锁
10.2.1.死锁现象
存在这样一种情况:一个线程需要同时获取多把锁。这个时候是很容易发生死锁的
- t1 线程 获得 A 对象 锁,接下来想获取 B对象 的锁
- t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁
测试代码
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
test1();
}
private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
控制台输出(由于多个线程由于存在环路的锁依赖关系而永远地等待下去)
19:04:55.361 c.TestDeadLock [t1] - lock A
19:04:55.361 c.TestDeadLock [t2] - lock B
// 俩线程都是一直在等待
10.2.2.定位死锁
检测死锁可以使用 jconsole 工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁
Found one Java-level deadlock:
=============================
"t2":
waiting to lock monitor 0x00000000264b1128 (object 0x000000071751f380, a java.lang.Object),
which is held by "t1"
"t1":
waiting to lock monitor 0x00000000264b3228 (object 0x000000071751f390, a java.lang.Object),
which is held by "t2"
Java stack information for the threads listed above:
===================================================
"t2":
at org.example.chapter04.activeness.TestDeadLock.lambda$test1$1(TestDeadLock.java:32)
- waiting to lock <0x000000071751f380> (a java.lang.Object)
- locked <0x000000071751f390> (a java.lang.Object)
at org.example.chapter04.activeness.TestDeadLock$$Lambda$2/1792845110.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1":
at org.example.chapter04.activeness.TestDeadLock.lambda$test1$0(TestDeadLock.java:21)
- waiting to lock <0x000000071751f390> (a java.lang.Object)
- locked <0x000000071751f380> (a java.lang.Object)
at org.example.chapter04.activeness.TestDeadLock$$Lambda$1/897913732.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
打印出来的信息还是蛮多的,具体操作和分析见视频:【活跃性 - 定位死锁】
避免死锁要注意加锁顺序
另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 Linux 下可以通过 top
先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id
来定位是哪个线程,最后再用 jstack
排查
10.2.3.哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 如果筷子被身边的人拿着,自己就得等待。
筷子类
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
哲学家类
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
synchronized (left) {
// 尝试获得右手筷子
synchronized (right) {
eat();
}
}
}
}
private void eat() {
log.debug("eating...");
Sleeper.sleep(1);
}
}
就餐(测试类)
public class TestDeadLock {
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("阿基米德", c1, c5).start();
}
}
执行不多会,就执行不下去了
12:33:15.575 [苏格拉底] c.Philosopher - eating...
12:33:15.575 [亚里士多德] c.Philosopher - eating...
12:33:16.580 [阿基米德] c.Philosopher - eating...
12:33:17.580 [阿基米德] c.Philosopher - eating...
// 卡在这里, 不向下运行
使用 jconsole
检测死锁,发现
-------------------------------------------------------------------------
名称: 阿基米德
状态: cn.itcast.Chopstick@1540e19d (筷子1) 上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 2, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@6d6f6e28 (筷子5)
-------------------------------------------------------------------------
名称: 苏格拉底
状态: cn.itcast.Chopstick@677327b6 (筷子2) 上的BLOCKED, 拥有者: 柏拉图
总阻止数: 2, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@1540e19d (筷子1)
-------------------------------------------------------------------------
名称: 柏拉图
状态: cn.itcast.Chopstick@14ae5a5 (筷子3) 上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@677327b6 (筷子2)
-------------------------------------------------------------------------
名称: 亚里士多德
状态: cn.itcast.Chopstick@7f31245a (筷子4) 上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 1, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@14ae5a5 (筷子3)
-------------------------------------------------------------------------
名称: 赫拉克利特
状态: cn.itcast.Chopstick@6d6f6e28 (筷子5) 上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@7f31245a (筷子4)
这种线程没有按预期结束,执行不下去的情况,归类为 活跃性 问题,除了死锁以外,还有 活锁 和 饥饿者 两种情况
10.3.活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
例
@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}
解决方法:要保证一方不能改变另一方的结束条件
- 使得线程的运行时间发生交错。
- 或者可以将睡眠时间调整为随机数。
10.4.饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束。
饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
这里举一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
顺序加锁的解决方案
11.ReentrantLock
共享模型之管程:ReentrantLock
重入锁可以完全替代 synchronized 关键字。
在 JDK 5.0 的早期版本中,重入锁的性能远远好于 synchronized。
但从 JDK 6.0 开始,JDK 在 synchronized 上做了大量的优化,使得两者的性能差距并不大。
重入锁使用 java.util.concurrent.locks.ReentrantLock
类来实现。
相对于 synchronized 它具备如下特点
- 与 synchronized 一样,都支持可重入
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
基本语法
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
对 ReentrantLock 的几个重要方法整理如下。
- lock():获得锁,如果锁已经被占用,则等待。
- lockInterruptibly():获得锁,但优先响应中断。
- tryLock():尝试获得锁,如果成功,返回 true,失败返回 false。该方法不等待,立即返回。
- tryLock(long time,TimeUnit unit):在给定时间内尝试获得锁。
- unlock():释放锁。
11.1.可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
示例代码:TestReentry.java
@Slf4j(topic = "c.TestReentry")
public class TestReentry {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
reentrantLock.lock();
try {
log.debug("enter main");
method_1();
} finally {
reentrantLock.unlock();
}
}
public static void method_1() {
reentrantLock.lock();
try {
log.debug("enter method_1");
method_2();
} finally {
reentrantLock.unlock();
}
}
private static void method_2() {
reentrantLock.lock();
try {
log.debug("enter method_2");
} finally {
reentrantLock.unlock();
}
}
}
控制台输出
09:39:14.440 [main] c.TestReentry - enter main
09:39:14.441 [main] c.TestReentry - enter method_1
09:39:14.441 [main] c.TestReentry - enter method_2
11.2.可打断
以下内容参考自 《实战 JAVA 高并发程序设计》 葛一鸣 著
对于 synchronized 来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。
而使用重入锁,则提供另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。
有些时候,这么做是非常有必要的。比如,如果你和朋友越好一起去打球。如果你等了半小时,朋友还未到。
突然接到了一个电话,说由于突发情况,不能如约了。那么你一定就扫兴地打道回府了。
中断正式提供了一套类似的机制。如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无须再等待,可以停止工作了。
lockInterruptibly() 方法,这是一个可以对中断进行响应的锁请求动作,即在等待锁的过程中,可以中断响应。
参考博客:https://blog.csdn.net/weixin_53142722/article/details/124566944
- 可打断指的是处于阻塞状态等待锁的线程可以被打断等待。
- 注意:lock.lockInterruptibly() 和 lock.trylock() 方法是可打断的,lock.lock() 不是。
- 可打断的意义在于避免得不到锁的线程无限制地等待下去,防止死锁的一种方式。
- 处于阻塞状态的线程,被打断了就不用阻塞了,我们可以在捕获打断异常后直接停止该线程的运行。
@Slf4j(topic = "c.TestInterrupt")
public class TestInterrupt {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
// 如果没有竞争,那么此方法会获取 lock 对象锁
// 如果有竞争,则会进入阻塞队列,可以被其他线程用 interrupt 方法打断
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(1);
t1.interrupt();
log.debug("执行打断");
} finally {
lock.unlock();
}
}
}
控制台输出
19:58:06.971 [main] c.TestInterrupt - 获得了锁
19:58:06.974 [t1] c.TestInterrupt - 启动...
19:58:07.981 [main] c.TestInterrupt - 执行打断
19:58:07.982 [t1] c.TestInterrupt - 等锁的过程中被打断
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 org.example.chapter04.reentrantLock.TestInterrupt.lambda$main$0(TestInterrupt.java:19)
at java.lang.Thread.run(Thread.java:748)
注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
11.3.锁超时
除了等待外部通知之外,要避免死锁还有另外一种方法,那就是限时等待。
我们可以使用 tryLock() 方法进行一次限时的等待。
原文链接:https://blog.csdn.net/weixin_53142722/article/details/124566944
- 使用 lock.tryLock() 方法会返回获取锁是否成功。返回一个布尔值,如果锁获取成功则返回 true,反之则返回 false。
- tryLock 方法还可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit),其中 timeout 为最长等待时间,TimeUnit 为时间单位。
- 如果 tryLock() 获取锁失败了、获取超时了或者被打断了,不再阻塞,线程直接停止运行。
- 也就是说在等待获取锁的过程中,这个方法也是支持可打断的。
示例代码(立即失败)
@Slf4j(topic = "c.TestTimeoutWaiting_1")
public class TestTimeoutWaiting_1 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
if (!lock.tryLock()) {
log.debug("获取立刻失败,返回");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(2);
} finally {
lock.unlock();
}
}
}
控制台输出
11:18:58.627 [main] c.TestTimeoutWaiting_1 - 获得了锁
11:18:58.630 [t1] c.TestTimeoutWaiting_1 - 启动...
11:18:58.631 [t1] c.TestTimeoutWaiting_1 - 获取立刻失败,返回
示例代码(超时失败)
@Slf4j(topic = "c.TestTimeoutWaitting_2")
public class TestTimeoutWaitting_2 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("获取等待 1s 后失败,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(2);
} finally {
lock.unlock();
}
}
}
控制台输出
11:21:57.510 [main] c.TestTimeoutWaitting_2 - 获得了锁
11:21:57.512 [t1] c.TestTimeoutWaitting_2 - 启动...
11:21:58.525 [t1] c.TestTimeoutWaitting_2 - 获取等待 1s 后失败,返回
示例代码(解决哲学家就餐问题)
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
if (left.tryLock()) {
try {
// 尝试获得右手筷子
if (right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock(); // 释放自己手里的筷子
}
}
}
}
Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}
@Slf4j(topic = "c.Test23")
public class Test23 {
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();
}
}
11.4.公平锁
以下内容参考自 《实战 JAVA 高并发程序设计》 葛一鸣 著
公平锁会按照时间的先后顺序,保证先到者先得,后到者后得。
公平锁的一大特点是:它不会产生饥饿现象。
如果我们使用 synchronized 关键字进行锁控制,那么产生的锁就是非公平的。
而重入锁允许我们对其公平性进行设置。它有一个如下的构造函数
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
当参数 fair 为 truef 时,表示锁是公平的。
公平锁看起来很优美,但是要实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能相对也非常低下。
在默认情况下,ReentrantLock 是非公平的。如果没有特别的需求,也不需要使用公平锁。
公平锁和非公平锁在线程调度表现上也是非常不一样的。
11.5.条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的
打个比方
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
相关方法
- await() 方法会使当前线程等待,同时释放当前锁,当其他线程中使用 signal() 方法(或 signalAll() 方法)时,线程会重新获得锁并继续执行。当线程被中断时,也可以跳出等待。该方法和 wait() 方法类似。
- signal() 方法用于唤醒一个在等待中的线程,signalAll() 方法会唤醒所有在等待中线程。该方法和 notify() 方法类似。
示例代码
@Slf4j(topic = "c.Test24")
public class Test24 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
static ReentrantLock ROOM = new ReentrantLock();
// 等待烟的休息室
static Condition waitCigaretteSet = ROOM.newCondition();
// 等外卖的休息室
static Condition waitTakeoutSet = ROOM.newCondition();
public static void main(String[] args) {
new Thread(() -> {
ROOM.lock();
try {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小南").start();
new Thread(() -> {
ROOM.lock();
try {
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
waitTakeoutSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小女").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasTakeout = true;
waitTakeoutSet.signal();
} finally {
ROOM.unlock();
}
}, "送外卖的").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasCigarette = true;
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
}, "送烟的").start();
}
}
控制台输出
20:08:58.861 c.Test24 [小南] - 有烟没?[false]
20:08:58.873 c.Test24 [小南] - 没烟,先歇会!
20:08:58.873 c.Test24 [小女] - 外卖送到没?[false]
20:08:58.873 c.Test24 [小女] - 没外卖,先歇会!
20:08:59.868 c.Test24 [小女] - 可以开始干活了
20:09:00.880 c.Test24 [小南] - 可以开始干活了
12.设计模式-2
共享模型之管程:设计模式-2
12.1.固定运行顺序
比如,必须先 2 后 1 打印
12.1.1.wait / notify
示例代码
@Slf4j(topic = "c.Test25")
public class Test25 {
static final Object lock = new Object();
// 表示 t2 是否运行过
static boolean t2runned = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (!t2runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock) {
log.debug("2");
t2runned = true;
lock.notify();
}
}, "t2");
t1.start();
t2.start();
}
}
控制台输出
21:43:14.751 c.Test25 [t2] - 2
21:43:14.754 c.Test25 [t1] - 1
12.1.2.await / signal
public static ReentrantLock reentrantLock = new ReentrantLock();
public static Condition condition = reentrantLock.newCondition();
// 表示 t2 是否运行过
static boolean t2runned = false;
public static void main(String[] args) {
new Thread(() -> {
reentrantLock.lock();
try {
while (!t2runned) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
reentrantLock.unlock();
}
log.debug("t1 ...");
}, "t1").start();
new Thread(() -> {
reentrantLock.lock();
try {
log.debug("t2 ...");
t2runned = true;
condition.signal();
} finally {
reentrantLock.unlock();
}
}, "t2").start();
}
控制台输出
22:13:08.329 [t2] c.Test25 - t2 ...
22:13:08.332 [t1] c.Test25 - t1 ...
12.1.3.park / unpark
Thread t1 = new Thread(() -> {
LockSupport.park();
log.debug("1");
}, "t1");
t1.start();
new Thread(() -> {
log.debug("2");
LockSupport.unpark(t1);
}, "t2").start();
输出
22:15:11.632 c.Test26 [t2] - 2
22:15:11.635 c.Test26 [t1] - 1
12.2.交替输出
线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。
现在要求输出 abcabcabcabcabc 怎么实现
12.2.1.wait / notify
class WaitNotify {
public void print(String str, int waitFlag, int nextFlag) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while(flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
// 等待标记
private int flag;
// 循环次数
private int loopNumber;
public WaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
}
@Slf4j(topic = "c.Test27")
public class Test27 {
public static void main(String[] args) {
WaitNotify wn = new WaitNotify(1, 5);
new Thread(() -> {
wn.print("a", 1, 2);
}).start();
new Thread(() -> {
wn.print("b", 2, 3);
}).start();
new Thread(() -> {
wn.print("c", 3, 1);
}).start();
}
}
12.2.2.await / signal
class AwaitSignal extends ReentrantLock {
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
//参数1 打印内容, 参数2 进入哪一间休息室, 参数3 下一间休息室
public void print(String str, Condition current, Condition next) {
for (int i = 0; i < loopNumber; i++) {
lock();
try {
current.await();
System.out.print(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock();
}
}
}
}
public class Test30 {
public static void main(String[] args) throws InterruptedException {
AwaitSignal awaitSignal = new AwaitSignal(5);
Condition a = awaitSignal.newCondition();
Condition b = awaitSignal.newCondition();
Condition c = awaitSignal.newCondition();
new Thread(() -> {
awaitSignal.print("a", a, b);
}).start();
new Thread(() -> {
awaitSignal.print("b", b, c);
}).start();
new Thread(() -> {
awaitSignal.print("c", c, a);
}).start();
Thread.sleep(1000);
awaitSignal.lock();
try {
System.out.println("开始...");
a.signal();
} finally {
awaitSignal.unlock();
}
}
}
12.2.3.park / unpark
class ParkUnpark {
public void print(String str, Thread next) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(next);
}
}
private int loopNumber;
public ParkUnpark(int loopNumber) {
this.loopNumber = loopNumber;
}
}
@Slf4j(topic = "c.Test31")
public class Test31 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
ParkUnpark pu = new ParkUnpark(5);
t1 = new Thread(() -> {
pu.print("a", t2);
});
t2 = new Thread(() -> {
pu.print("b", t3);
});
t3 = new Thread(() -> {
pu.print("c", t1);
});
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
}
13.本章总结
本章我们需要重点掌握的是
- 分析多线程访问共享资源时,哪些代码片段属于临界区
- 使用 synchronized 互斥解决临界区的线程安全问题
- 掌握 synchronized 锁对象语法
- 掌握 synchronized 加载成员方法和静态方法语法
- 掌握 wait/notify 同步方法
- 使用 lock 互斥解决临界区的线程安全问题
- 掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
- 学会分析变量的线程安全性、掌握常见线程安全类的使用
- 了解线程活跃性问题:死锁、活锁、饥饿
- 应用方面
- 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
- 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
- 原理方面
- monitor、synchronized、wait/notify 原理
- synchronized 进阶原理
- park & unpark 原理
- 模式方面
- 同步模式之保护性暂停
- 异步模式之生产者消费者
- 同步模式之顺序控制