Java并发编程学习笔记3——synchronized原理
目录
7.3.1、sleep(long n) 和 wait(long n)的区别
9.3.3、情况3:RUNNABLE <--> WAITING
9.3.4、情况4:RUNNABLE <--> WAITING
9.3.5、情况5:RUNNABLE <--> TIMED_WAITING
9.3.6、情况6:RUNNABLE <--> TIMED_WAITING
9.3.7、情况7:RUNNABLE <--> TIMED_WAITING
9.3.8、情况8:RUNNABLE <--> TIMED_WAITING
9.3.9、情况9:RUNNABLE <--> BLOCKED
9.3.10、情况10:RUNNABLE <--> TERMINATED
13.2.2、await & signal 版(ReentranLock)
1、共享带来的问题
两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0吗?
@Slf4j
public class Test1 {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", count);
}
}
结果:
14:37:13.111 [main] DEBUG com.bfbc.test3.Test1 - -2314
1.1、临界区(Critical Section)
一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源。多个线程读共享资源其实也没有问题,但在多个线程对共享资源读写操作时发生指令交错,就会出现问题。一段代码块内如果存在对共享资源的多线程读写操作,那么称这段代码块为临界区。
例如,下面代码中的临界区:
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
1.2、竞态条件(Race Condition)
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
2、synchronize解决方案
2.1、应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量。
本次使用synchronized来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获得这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全地执行临界区内的代码,不用担心线程上下文切换。
注意:虽然Java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的。
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码;
- 同步是由于线程执行的先后、顺序不同,需要一个线程等待其他线程运行到某个点。
2.2、synchronized
语法:
synchronized(对象) {
临界区
}
解决:
@Slf4j
public class Test1 {
static int count = 0;
private 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) {
count++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
count--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", count);
}
}
结果;
15:01:37.907 [main] DEBUG com.bfbc.test3.Test1 - 0
synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
2.3、面向对象改进
把需要保护的共享变量放入一个类。
public class Room {
int value = 0;
public void increment() {
synchronized (this) {
value ++;
}
}
public void decrement() {
synchronized (this) {
value --;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
@Slf4j
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.get());
}
}
结果:
15:12:14.613 [main] DEBUG com.bfbc.test3.Test1 - 0
3、方法上的synchronized
class Test {
public synchronized void test() {
}
}
等价于:
class Test {
public void test() {
synchronized(this) {
}
}
}
class Test {
public synchronized static void test() {
}
}
等价于:
class Test {
public static void test() {
synchronized(Test.class) {
}
}
}
不加synchronized的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)。
4、变量的线程安全分析
4.1、成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全;
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况:如果只有读操作,则线程安全;如果有读写操作,则这段代码就是临界区,需要考虑线程安全。
4.2、局部变量是否线程安全?
- 局部变量是线程安全的;
- 但局部变量引用的对象则未必:如果该对象没有逃离方法的作用访问,它是线程安全的;如果该对象逃离方法的作用范围,需要考虑线程安全。
4.3、常见线程安全类
String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent包下的类。
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的,也可以理解为:它们的每个方法是原子的,但注意它们多个方法的组合不是原子的。
Hashtable table = new Hashtable();
new Thread(() -> {
table.put("key", "value1);
});
new Thread(() -> {
tbale.put("key", "value2");
});
4.3.1、线程安全类方法的组合
分析下面代码是否线程安全?
Hashtable table = new Hashtable();
// 线程1,线程2
if (table.get("key") == null) {
table.put("key", value);
}
4.3.2、不可变类线程安全性
String、Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。
5、Monitor(锁)概念
5.1、Java对象头
以64位虚拟机为例。
5.2、Monitor(锁)
Monitor被翻译为监视器或管程。
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向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的对象不会关联监视器,不遵从以上规则。
6、synchronized原理进阶
6.1、轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么就可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized。
// 假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized (obj) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized (obj) {
// 同步块 B
}
}
1、创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word;
2、让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录;
3、如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下:
4、如果cas替换失败,有两种情况:
- 如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程;
- 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数。
5、当退出synchronized代码块(解锁)时,如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。
6、当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头。成功,则解锁成功;失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
6.2、锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized(obj) {
// 同步块
}
}
1、当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁。
这时Thread-1加轻量级锁失败,进入锁膨胀流程:
- 即为Object对象申请Monitor锁,让Object指向重量级锁地址;
- 然后自己进入Monitor的EntryList BLOCKED。
2、当Thread-0退出同步块解锁时,使用CAS将Mark Word的值回复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程。
6.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(重量锁) 重量锁指针 | 阻塞 |
- | ... | ... |
- 在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能;
- 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势;
- Java 7之后不能控制是否开启自旋功能。
6.4、偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然要执行CAS操作。
Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。
6.4.1、偏向状态
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后3位为101,这时它的thread、epoch、age都为0;
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想要避免延迟,可以加VM参数 - xx:BiasedLockingStartupDelay=0来禁用延迟;
- 如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值;
6.4.2、偏向锁撤销 - 调用对象hashCode
调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是线程id,如果调用hashCode会导致偏向锁被撤销。
- 轻量级锁会在锁记录中记录hashCode;
- 重量级锁会在Monitor中记录hashCode;
在调用hashCode后使用偏向锁,记得去掉 -xx:-UseBiasedLocking
6.4.3、偏向锁撤销 - 其它线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。
@Slf4j
public class TestBiased2 {
public static void main(String[] args) {
Dog dog = new Dog();
Thread t1 = new Thread(()->{
log.debug(ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog) {
log.debug(ClassLayout.parseInstance(dog).toPrintable());
}
log.debug(ClassLayout.parseInstance(dog).toPrintable());
synchronized (TestBiased2.class) {
TestBiased2.class.notify();
}
},"t1");
t1.start();
Thread t2 = new Thread(()->{
synchronized (TestBiased2.class) {
try {
TestBiased2.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug(ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog) {
log.debug(ClassLayout.parseInstance(dog).toPrintable());
}
log.debug(ClassLayout.parseInstance(dog).toPrintable());
},"t2");
t2.start();
}
}
6.4.4、偏向锁撤销 - 调用wait/notify
6.4.5、批量重定向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID。当撤销偏向锁阈值超过20次后,JVM会在给这些对象加锁时重新偏向至加锁线程。
@Slf4j
public class TestBiased3 {
public static void main(String[] args) {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(()->{
for (int i=0; i<30; i++) {
Dog dog = new Dog();
list.add(dog);
synchronized (dog) {
log.debug(i + "\t" + ClassLayout.parseInstance(dog).toPrintable());
}
}
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 dog = list.get(i);
log.debug(i + "\t" +ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog) {
log.debug((i + "\t" + ClassLayout.parseInstance(dog).toPrintable()));
}
log.debug(i + "\t" + ClassLayout.parseInstance(dog).toPrintable());
}
}, "t2");
t2.start();
}
}
6.4.6、批量撤销
当撤销偏向锁阈值超过40次后,JVM会这样觉得,自己确实是偏向错了,根本就不该偏向。于是整个类的所有对象都变为不可偏向的,新建的对象也是不可偏向的。
7、wait() / notify()
7.1、wait() / notify() 原理
- Owner线程发现条件不满足,调用wait()方法,即可进入WaitSet变为WAITING状态;
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片;
- BLOCKED线程会在Owner线程释放锁时唤醒;
- WAITING线程会在Owner线程调用notify()或notifyAll()时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争。
7.2、API介绍
- obj.wait()让进入object监视器的线程到waitSet等待;
- obj.notify()在object上正在waitSet等待的线程中挑一个唤醒;
- obj.notifyAll()让object上正在waitSet等待的线程全部唤醒。
它们都是线程之间进行协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法。
@Slf4j
public class TestBiased4 {
final static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (lock) {
log.debug("执行...");
try {
lock.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其他代码...");
}
}, "t1");
t1.start();
Thread t2 = new Thread(()->{
synchronized (lock) {
log.debug("执行...");
try {
lock.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其他代码...");
}
}, "t2");
t2.start();
// 主线程两秒后执行
Thread.sleep(2000);
log.debug("唤醒 obj 上其他线程");
synchronized (lock) {
lock.notify(); // 唤醒lock上的一个线程
// lock.notifyAll(); // 唤醒lock上的所有线程
}
}
}
结果:
16:51:38.839 [t1] DEBUG com.bfbc.test4.TestBiased4 - 执行...
16:51:38.841 [t2] DEBUG com.bfbc.test4.TestBiased4 - 执行...
16:51:40.837 [main] DEBUG com.bfbc.test4.TestBiased4 - 唤醒 obj 上其他线程
16:51:40.837 [t1] DEBUG com.bfbc.test4.TestBiased4 - 其他代码...
7.3、wait() / notify() 的正确使用姿势
7.3.1、sleep(long n) 和 wait(long n)的区别
- sleep是Thread方法,而wait是Object的方法;
- sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起使用;
- sleep在睡眠的同时,不会释放对象锁,但wait在等待的时候会释放对象锁。
@Slf4j
public class TestBiased5 {
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (lock) {
log.debug("获得锁");
try {
Thread.sleep(10000);
// lock.wait(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
synchronized (lock) {
log.debug("获得锁");
}
}
}
结果:
17:08:46.939 [t1] DEBUG com.bfbc.test4.TestBiased5 - 获得锁
17:08:47.945 [main] DEBUG com.bfbc.test4.TestBiased5 - 获得锁
@Slf4j
public class TestBiased6 {
static final Object room = new Object();
static boolean hasCigarette = false; // 有没有烟
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了...");
}
}
}, "小南");
t1.start();
for (int i = 0; i < 5; i++) {
new Thread(()->{
synchronized (room) {
log.debug("可以开始干活了");
}
},"其他人").start();
}
Thread.sleep(1000);
new Thread(()->{
hasCigarette = true;
log.debug("烟到了");
}, "送烟的").start();
}
}
结果:
18:41:54.749 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 有烟没?[false]
18:41:54.752 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 没烟,先歇会!
18:41:55.748 [送烟的] DEBUG com.multiThreads.test4.TestBiased6 - 烟到了
18:41:56.753 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 有烟没?[true]
18:41:56.753 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了...
18:41:56.753 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
18:41:56.753 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
18:41:56.753 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
18:41:56.753 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
18:41:56.753 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
其他干活的线程,都要一直阻塞,效率太低;
- 小南线程必须睡足2s后才能醒来,就算烟提前送到,也无法立刻醒来;
- 加了synchronized(room)之后,就好比小南在房间里面反锁了门睡觉,烟根本无法送进门,main没加synchronized就好像main线程是翻窗户进来的;
- 解决方法:使用wait-notify机制。
解决方案:
@Slf4j
public class TestBiased6 {
static final Object room = new Object();
static boolean hasCigarette = false; // 有没有烟
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = 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("可以开始干活了...");
}
}
}, "小南");
t1.start();
for (int i = 0; i < 5; i++) {
new Thread(()->{
synchronized (room) {
log.debug("可以开始干活了");
}
},"其他人").start();
}
Thread.sleep(1000);
new Thread(()->{
synchronized (room) {
hasCigarette = true;
log.debug("烟到了");
room.notify();
}
}, "送烟的").start();
}
}
结果;
18:51:26.549 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 有烟没?[false]
18:51:26.552 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 没烟,先歇会!
18:51:26.553 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
18:51:26.553 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
18:51:26.553 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
18:51:26.553 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
18:51:26.553 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
18:51:27.557 [送烟的] DEBUG com.multiThreads.test4.TestBiased6 - 烟到了
18:51:27.557 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 有烟没?[true]
18:51:27.558 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了...
解决了其他干活的线程阻塞的问题。但如果有其他线程也在等待条件呢?
@Slf4j
public class TestBiased6 {
static final Object room = new Object();
static boolean hasCigarette = false; // 有没有烟
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = 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("没干成活...");
}
}
}, "小南");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (room) {
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("没干成活...");
}
}
}, "小女");
t2.start();
for (int i = 0; i < 5; i++) {
new Thread(()->{
synchronized (room) {
log.debug("可以开始干活了");
}
},"其他人").start();
}
// 虚假唤醒
Thread.sleep(1000);
new Thread(()->{
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了");
room.notify();
}
}, "送外卖的").start();
}
}
结果:
19:01:25.025 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 有烟没?[false]
19:01:25.030 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 没烟,先歇会!
19:01:25.030 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
19:01:25.030 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
19:01:25.030 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
19:01:25.030 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
19:01:25.030 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
19:01:25.030 [小女] DEBUG com.multiThreads.test4.TestBiased6 - 有外卖没?[false]
19:01:25.030 [小女] DEBUG com.multiThreads.test4.TestBiased6 - 没外卖,先歇会!
19:01:26.024 [送外卖的] DEBUG com.multiThreads.test4.TestBiased6 - 外卖到了
19:01:26.024 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 有烟没?[false]
19:01:26.024 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 没干成活...
解决虚假唤醒:
@Slf4j
public class TestBiased6 {
static final Object room = new Object();
static boolean hasCigarette = false; // 有没有烟
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = 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("没干成活...");
}
}
}, "小南");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (room) {
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("没干成活...");
}
}
}, "小女");
t2.start();
for (int i = 0; i < 5; i++) {
new Thread(()->{
synchronized (room) {
log.debug("可以开始干活了");
}
},"其他人").start();
}
// 虚假唤醒
Thread.sleep(1000);
new Thread(()->{
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了");
room.notifyAll();
}
}, "送外卖的").start();
}
}
结果:
19:05:30.114 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 有烟没?[false]
19:05:30.118 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 没烟,先歇会!
19:05:30.118 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
19:05:30.118 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
19:05:30.118 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
19:05:30.118 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
19:05:30.118 [其他人] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了
19:05:30.119 [小女] DEBUG com.multiThreads.test4.TestBiased6 - 有外卖没?[false]
19:05:30.119 [小女] DEBUG com.multiThreads.test4.TestBiased6 - 没外卖,先歇会!
19:05:31.114 [送外卖的] DEBUG com.multiThreads.test4.TestBiased6 - 外卖到了
19:05:31.115 [小女] DEBUG com.multiThreads.test4.TestBiased6 - 有外卖没?[true]
19:05:31.115 [小女] DEBUG com.multiThreads.test4.TestBiased6 - 可以开始干活了...
19:05:31.115 [小南] DEBUG com.multiThreads.test4.TestBiased6 - 没烟,先歇会!
7.3.2、使用wait() / notify() 的套路
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 条件成立,继续执行
{
// 另一个线程使用 notifyAll()唤醒
synchronized(lock) {
lock.notifyAll();
}
7.3.3、同步模式——保护性暂停
保护性暂停(Guarded Suspension),用在一个线程等待另一个线程的执行结果。
要点:
- 有一个结果需要从一个线程传到另一个线程,让他们关联同一个GuardedObject;
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者);
- JDK中,join的实现、Future的实现,采用的就是此模式;
- 因为要等待另一方的结果,因此归类到同步模式。
代码演示:
package com.multiThreads.test4;
import lombok.extern.slf4j.Slf4j;
class GuardedObject {
private Object response;
// 获取结果,timeOut:最大等待时间,超过这个时间即使没有拿到response的值也退出。
public Object get(long timeOut) {
synchronized (this) {
// 开始时间
long beginTime = System.currentTimeMillis();
// 经历的时间
long passedTime = 0;
// 没有结果
while(response == null) {
// 经历的时间超过timeOut时,退出循环
// waitTime:这一轮循环应等待的时间
long waitTime = timeOut - passedTime;
if (waitTime <= 0) break;
try {
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 求得经历时间
passedTime = System.currentTimeMillis() - beginTime;
}
}
return response;
}
// 产生结果
public void set(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}
@Slf4j
public class Test2 {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
// 线程1等待线程2的下载结果
new Thread(()->{
log.debug("等待结果");
String result = (String)guardedObject.get(3000);
log.debug("结果是:" + result);
}, "t1").start();
new Thread(()->{
log.debug("执行下载...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
guardedObject.set(null);
}, "t2").start();
}
}
结果:
19:50:57.549 [t1] DEBUG com.multiThreads.test4.Test2 - 等待结果
19:50:57.549 [t2] DEBUG com.multiThreads.test4.Test2 - 执行下载...
19:51:00.566 [t1] DEBUG com.multiThreads.test4.Test2 - 结果是:null
7.3.4、扩展
图中Futures就好比居民楼一层的信箱(每个信箱都有房间编号),左侧的t0,t2,t4就好比等待邮件的居民,右侧的t1,t3,t5就好比邮递员。
如果需要在多个类之间使用GuardedObject对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待着】和【结果生产者】,还能够同时支持多个任务的管理。
public class Test {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new People().start(); // 居民
}
Thread.sleep(1000);
for (Integer id : MailBoxes.getIds()) {
new Postman(id, "内容" + id).start(); // 邮递员
}
}
}
// 居民类
class People extends Thread{
private static final Logger LOGGER = LoggerFactory.getLogger(People.class);
@Override
public void run() {
// 收信
GuardedObject guardedObject = MailBoxes.createGuardedObject();
LOGGER.debug("开始收信 id:{}", guardedObject.getId());
Object mail = guardedObject.get(5000);
LOGGER.debug("收到信 id:{},内容:{}", guardedObject.getId(), mail);
}
}
// 邮递员类
class Postman extends Thread{
private static final Logger LOGGER = LoggerFactory.getLogger(Postman.class);
private int id;
private String mail;
public Postman(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
GuardedObject guardedObject = MailBoxes.getGuardedObject(id);
LOGGER.debug("送信 id:{},内容:{}", guardedObject.getId(), mail);
guardedObject.set(mail);
}
}
class MailBoxes {
private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
private static int id = 1;
// 产生唯一id
private static synchronized int generateId() {
return id++;
}
public static GuardedObject getGuardedObject(int id) {
return boxes.remove(id);
}
public static GuardedObject createGuardedObject() {
GuardedObject go = new GuardedObject(generateId());
boxes.put(go.getId(), go);
return go;
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
}
//
class GuardedObject {
// 标识Guarded Object
private int id;
public int getId() {return id;}
// 结果
private Object response;
public GuardedObject(int id) {
this.id = id;
}
// 获取结果,timeOut:最大等待时间,超过这个时间即使没有拿到response的值也退出。
public Object get(long timeOut) {
synchronized (this) {
// 开始时间
long beginTime = System.currentTimeMillis();
// 经历的时间
long passedTime = 0;
// 没有结果
while(response == null) {
// 经历的时间超过timeOut时,退出循环
// waitTime:这一轮循环应等待的时间
long waitTime = timeOut - passedTime;
if (waitTime <= 0) break;
try {
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 求得经历时间
passedTime = System.currentTimeMillis() - beginTime;
}
}
return response;
}
// 产生结果
public void set(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}
结果;
19:23:24.248 [Thread-2] DEBUG com.multiThreads.test5.People - 开始收信 id:2
19:23:24.248 [Thread-0] DEBUG com.multiThreads.test5.People - 开始收信 id:3
19:23:24.248 [Thread-1] DEBUG com.multiThreads.test5.People - 开始收信 id:1
19:23:25.253 [Thread-3] DEBUG com.multiThreads.test5.Postman - 送信 id:3,内容:内容3
19:23:25.253 [Thread-4] DEBUG com.multiThreads.test5.Postman - 送信 id:2,内容:内容2
19:23:25.253 [Thread-5] DEBUG com.multiThreads.test5.Postman - 送信 id:1,内容:内容1
19:23:25.253 [Thread-2] DEBUG com.multiThreads.test5.People - 收到信 id:2,内容:内容2
19:23:25.253 [Thread-1] DEBUG com.multiThreads.test5.People - 收到信 id:1,内容:内容1
19:23:25.253 [Thread-0] DEBUG com.multiThreads.test5.People - 收到信 id:3,内容:内容3
7.3.5、异步模式之生产者/消费者
要点:
- 与前面的保护性暂停中的GuardedObject不同,不需要产生结果和消费结果的线程一一对应;
- 消费队列可以用来平衡生产和消费的线程资源;
- 生产者仅负责生产结果数据,不关心数据该如何处理,而消费者专心处理结果数据;
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据;
- JDK中各种阻塞队列,采用的就是这种模式。
代码演示:
public class Test2 {
public static void main(String[] args) throws InterruptedException {
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) {
try {
Thread.sleep(1000);
Message message = messageQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者线程").start();
}
}
// 消息队列类,Java线程之间通信
class MessageQueue {
private static final Logger LOGGER = LoggerFactory.getLogger(MessageQueue.class);
// 消息的队列集合
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 {
LOGGER.debug("队列为空,消费者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 从队列的头部获取消息并返回
Message message = list.removeFirst();
LOGGER.debug("已消费消息: {}", message);
list.notifyAll();
return message;
}
}
// 存入消息
public void put(Message message) {
// 检查队列是否为满
synchronized (list) {
while (list.size() == capacity) {
try {
LOGGER.debug("队列已满,生产者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 将新的消息加入队列的尾部
list.addLast(message);
LOGGER.debug("已生产消息: {}", message);
list.notifyAll();
}
}
}
// 设计Message
@Data
final class Message {
private int id;
private Object value;
public Message(int id, Object value) {
this.id = id;
this.value = value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
结果:
20:03:08.145 [生产者线程0] DEBUG com.multiThreads.test5.MessageQueue - 已生产消息: Message{id=0, value=值0}
20:03:08.145 [生产者线程1] DEBUG com.multiThreads.test5.MessageQueue - 已生产消息: Message{id=1, value=值1}
20:03:08.145 [生产者线程2] DEBUG com.multiThreads.test5.MessageQueue - 队列已满,生产者线程等待
20:03:09.147 [消费者线程] DEBUG com.multiThreads.test5.MessageQueue - 已消费消息: Message{id=0, value=值0}
20:03:09.147 [生产者线程2] DEBUG com.multiThreads.test5.MessageQueue - 已生产消息: Message{id=2, value=值2}
20:03:10.148 [消费者线程] DEBUG com.multiThreads.test5.MessageQueue - 已消费消息: Message{id=1, value=值1}
20:03:11.149 [消费者线程] DEBUG com.multiThreads.test5.MessageQueue - 已消费消息: Message{id=2, value=值2}
20:03:12.150 [消费者线程] DEBUG com.multiThreads.test5.MessageQueue - 队列为空,消费者线程等待
9、park() / unpark()
9.1、基本使用
它们是LockSupport类中的方法。
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpack();
public class TestParkUnpark {
private static final Logger LOGGER = LoggerFactory.getLogger(TestParkUnpark.class);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
LOGGER.debug("start");
Thread.sleep(1000);
LOGGER.debug("park");
LockSupport.park(); // 对应线程状态为wait
LOGGER.debug("resume");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(2000);
LOGGER.debug("unpark..");
LockSupport.unpark(t1);
}
}
结果:
14:37:09.913 [Thread-0] DEBUG com.multiThreads.test6.TestParkUnpark - start
14:37:10.915 [Thread-0] DEBUG com.multiThreads.test6.TestParkUnpark - park
14:37:11.912 [main] DEBUG com.multiThreads.test6.TestParkUnpark - unpark..
14:37:11.912 [Thread-0] DEBUG com.multiThreads.test6.TestParkUnpark - resume
9.2、特点
与Object的wait & notify 相比:
- wait,notify和notifyAll必须配合Object Monitor一起使用,而unpark不必;
- park & unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】;
- park & unpark可以先unpark,而wait & notify不能先notify。
9.3、原理之park & unpark
每个线程都有自己的一个Parker对象,由三部分组成_counter, _cond 和 _mutex。打个比喻:
- 线程就像一个旅人,Parker就像他随身携带的背包,条件变量就好比背包中的帐篷,_counter就好比背包中的备用干粮(0为耗尽,1为充足);
- 调用park就是要看需不需要停下来休息:① 如果备用干粮耗尽,那么钻进帐篷休息;如果备用干粮充足,那么不需停留,继续前进;
- 调用unpark,就好比令干粮充足:① 如果这时线程还在帐篷,就唤醒让他继续前进;② 如果这时线程还在运行,那么下次它调用park时,仅是消耗掉备用干粮,不需停留继续前进,因为背包空间有限,多次调用unpark仅会补充一份备用干粮。
假设有线程Thread t。
9.3.1、情况1:NEW -> RUNNABLE
当调用t.start()方法时,由NEW -> RUNNABLE
9.3.2、情况2:RUNNABLE -> WAITING
t线程用synchronized(obj)获取了对象锁后:
- 调用obj.wait()方法时,t线程从RUNNABLE -> WAITING;
- 调用obj.notify(),obj.notifyAll(),t.interrupt()时:① 竞争锁成功,t线程从WAITING -> RUNNABLE;② 竞争锁失败,t线程从WAITING -> BLOCKED。
public class TestWaitNotify {
private static final Logger LOGGER = LoggerFactory.getLogger(TestWaitNotify.class);
private final static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()-> {
synchronized (obj) {
LOGGER.debug("执行...");
try {
obj.wait();
LOGGER.debug("其他代码...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
new Thread(()-> {
synchronized (obj) {
LOGGER.debug("执行...");
try {
obj.wait();
LOGGER.debug("其他代码...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t2").start();
// 主线程两秒后执行
Thread.sleep(500);
LOGGER.debug("唤醒obj上其他线程");
synchronized (obj) {
obj.notifyAll();
}
}
}
结果:
15:43:40.822 [t1] DEBUG com.multiThreads.test6.TestWaitNotify - 执行...
15:43:40.825 [t2] DEBUG com.multiThreads.test6.TestWaitNotify - 执行...
15:43:41.320 [main] DEBUG com.multiThreads.test6.TestWaitNotify - 唤醒obj上其他线程
15:43:41.320 [t2] DEBUG com.multiThreads.test6.TestWaitNotify - 其他代码...
15:43:42.321 [t1] DEBUG com.multiThreads.test6.TestWaitNotify - 其他代码...
9.3.3、情况3:RUNNABLE <--> WAITING
- 当前线程调用t.join()方法时,当前线程从RUNNABLE --> WAITING(注意是当前线程在t线程对象的监视器上等待);
- t线程结束时,或调用了当前线程的interrupt()时,当前线程从WAITING --> RUNNABLE。
9.3.4、情况4:RUNNABLE <--> WAITING
- 当前线程调用LockSupport.park()方法会让当前线程从RUNNABLE --> WAITING;
- 调用LockSupport.unpark(目标线程)或调用了线程的interrupt(),会让目标线程从WAITING --> RUNNABLE。
9.3.5、情况5:RUNNABLE <--> TIMED_WAITING
t线程用synchronized(obj)获取了对象锁后:
- 调用obj.wait(long n)方法时,t线程从RUNNABLE --> TIMED_WAITING;
- t线程等待时间超过了n毫秒,或调用obj.notify(),obj.notifyAll(),t.interrupt()时:① 竞争锁成功,t线程从TIMED_WAITING --> RUNNABLE;② 竞争锁失败:t线程从TIMED_WAITING --> BLOCKED。
9.3.6、情况6:RUNNABLE <--> TIMED_WAITING
- 当前线程调用t.join(long n)方法时,当前线程从RUNNABLE --> TIMED_WAITING(注意是当前线程在t线程对象的监视器上等待);
- 当前线程等待时间超过了n毫秒,或t线程运行结束,或调用了当前线程的interrupt()时,当前线程从TIMED_WAITING --> RUNNABLE。
9.3.7、情况7:RUNNABLE <--> TIMED_WAITING
- 当前线程调用Thread.sleep(long n),当前线程从RUNNABLE --> TIMED_WAITING;
- 当前线程等待时间超过了n毫秒,当前线程从TIMED_WAITING --> RUNNABLE。
9.3.8、情况8:RUNNABLE <--> TIMED_WAITING
- 当前线程调用LockSupport.parkNanos(long nanos)或LockSupport.parkUntil(long millis)时,当前线程从RUNNABLE --> TIMED_WAITING;
- 调用LockSupport.unpark(目标线程)或调用了线程的interrupt(),或是等待超时,会让目标线程从TIMED_WAITING --> RUNNABLE。
9.3.9、情况9:RUNNABLE <--> BLOCKED
- t线程用synchronized(obj)获取了对象锁时如果竞争失败,从RUNNABLE --> BLOCKED;
- 持obj锁线程的同步代码块执行完毕,会唤醒该对象上所有BLOCKED的线程重新竞争,如果其中t线程。
9.3.10、情况10:RUNNABLE <--> TERMINATED
当前线程所有代码运行完毕,进入TERMINATED。
10、多把锁
10.1、多把不相干的锁
一间大屋子有两个功能:睡觉、学习。互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低。解决的方法是准备多个房间(多个对象锁)。
例如:
public class TestMultiLock {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.study();
}, "小南").start();
new Thread(() -> {
bigRoom.sleep();
}, "小女").start();
}
}
class BigRoom {
private static final Logger LOGGER = LoggerFactory.getLogger(BigRoom.class);
private final Object studyRoom = new Object();
private final Object bedRoom = new Object();
// 睡觉
public void sleep() {
synchronized (bedRoom) {
LOGGER.debug("sleeping 2 秒");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 学习
public void study() {
synchronized (studyRoom) {
LOGGER.debug("study 1 秒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果:
16:29:52.438 [小女] DEBUG com.multiThreads.test7.BigRoom - sleeping 2 秒
16:29:52.438 [小南] DEBUG com.multiThreads.test7.BigRoom - study 1 秒
将锁的粒度细分:
- 好处:可以增加并发度;
- 坏处:如果一个线程需要同时获得多把锁,就容易发生死锁。
11、活跃性
11.1、死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁。
- t1线程获得A对象锁,接下来想获取B对象的锁;
- t2线程获得B对象锁,接下来想获取A对象的锁。
例:
public class DeadLock {
private static final Logger LOGGER = LoggerFactory.getLogger(DeadLock.class);
private static Object A = new Object();
private static Object B = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized (A) {
LOGGER.debug("Lock A");
sleep(1);
synchronized (B) {
LOGGER.debug("Lock B");
LOGGER.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(()->{
synchronized (B) {
LOGGER.debug("Lock B");
sleep(0.5);
synchronized (A) {
LOGGER.debug("Lock B");
LOGGER.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
private static void sleep(double time) {
try {
Thread.sleep((long)(time * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
18:30:00.623 [t1] DEBUG com.multiThreads.test8.DeadLock - Lock A
18:30:00.623 [t2] DEBUG com.multiThreads.test8.DeadLock - Lock B
11.2、定位死锁
检测死锁可以使用jconsole工具,或者使用jps定位进程id,再用jstack定位死锁。
cmd > jps
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
12320 Jps
22816 KotlinCompileDaemon
33200 TestDeadLock // JVM进程
11508 Main
28468 Launcher
11.3、哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
- 它们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考;
- 吃饭时要用两根筷子吃,桌上共有5根筷子,每位哲学家左右手边各有一根筷子;
- 如果筷子被身边的人拿着,自己就得等待。
筷子类:
public class TestPhilosopher {
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();
}
}
/**
* 哲学家就餐问题
*/
class Philosopher extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Philosopher.class);
private Chopstick left;
private 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() {
LOGGER.debug("eating...");
sleep0(1);
}
private void sleep0(double time) {
try {
Thread.sleep((long)(time * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + "}";
}
}
结果:
18:53:55.500 [苏格拉底] DEBUG com.multiThreads.test8.Philosopher - eating...
18:53:55.500 [亚里士多德] DEBUG com.multiThreads.test8.Philosopher - eating...
18:53:56.504 [亚里士多德] DEBUG com.multiThreads.test8.Philosopher - eating...
18:53:56.504 [阿基米德] DEBUG com.multiThreads.test8.Philosopher - eating...
18:53:57.504 [阿基米德] DEBUG com.multiThreads.test8.Philosopher - eating...
18:53:57.504 [柏拉图] DEBUG com.multiThreads.test8.Philosopher - eating...
18:53:58.504 [柏拉图] DEBUG com.multiThreads.test8.Philosopher - eating...
18:53:58.504 [赫拉克利特] DEBUG com.multiThreads.test8.Philosopher - eating...
18:53:59.505 [亚里士多德] DEBUG com.multiThreads.test8.Philosopher - eating...
18:53:59.505 [苏格拉底] DEBUG com.multiThreads.test8.Philosopher - eating...
18:54:00.506 [阿基米德] DEBUG com.multiThreads.test8.Philosopher - eating...
18:54:00.506 [柏拉图] DEBUG com.multiThreads.test8.Philosopher - eating...
18:54:01.506 [柏拉图] DEBUG com.multiThreads.test8.Philosopher - eating...
18:54:01.506 [赫拉克利特] DEBUG com.multiThreads.test8.Philosopher - eating...
18:54:02.506 [柏拉图] DEBUG com.multiThreads.test8.Philosopher - eating...
18:54:02.506 [赫拉克利特] DEBUG com.multiThreads.test8.Philosopher - eating...
// 此处发生了死锁,程序停在这里
11.4、活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如:
public class TestLiveLock {
private static final Logger LOGGER = LoggerFactory.getLogger(TestLiveLock.class);
static volatile int count = 0;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(()->{
// 期望减到-10退出循环
while(count > -10) {
sleep(0.2);
count--;
LOGGER.debug("count: {}", count);
}
},"t1").start();
new Thread(()->{
// 期望超过10退出循环
while(count < 10) {
sleep(0.2);
count++;
LOGGER.debug("count: {}", count);
}
},"t2").start();
}
private static void sleep(double time) {
try {
Thread.sleep((long) (time * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
19:06:09.100 [t2] DEBUG com.multiThreads.test9.TestLiveLock - count: 0
19:06:09.100 [t1] DEBUG com.multiThreads.test9.TestLiveLock - count: -1
19:06:09.306 [t2] DEBUG com.multiThreads.test9.TestLiveLock - count: 1
19:06:09.306 [t1] DEBUG com.multiThreads.test9.TestLiveLock - count: 0
19:06:09.506 [t1] DEBUG com.multiThreads.test9.TestLiveLock - count: -1
... // 一直在0左右徘徊
11.5、饥饿
很多教程中把饥饿定义为:一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束,饥饿的情况不易演示。
12、ReentranLock
相对于synchronized,ReentranLock具备如下特点:
- 可中断;
- 可以设置超时时间;
- 可以设置为公平锁;
- 支持多个条件变量。
与synchronized一样,都支持可重入。
12.1、可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁;如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
public class TestReentrantLock {
private static final Logger LOGGER = LoggerFactory.getLogger(TestReentrantLock.class);
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
private static void method1() {
// 获取锁
lock.lock();
try {
// 临界区
LOGGER.debug("execute method1");
method2();
} finally {
// 释放锁
lock.unlock();
}
}
private static void method2() {
// 获取锁
lock.lock();
try {
// 临界区
LOGGER.debug("execute method2");
method3();
} finally {
// 释放锁
lock.unlock();
}
}
private static void method3() {
// 获取锁
lock.lock();
try {
// 临界区
LOGGER.debug("execute method3");
} finally {
// 释放锁
lock.unlock();
}
}
}
结果:
19:29:27.249 [main] DEBUG com.multiThreads.test10.TestReentrantLock - execute method1
19:29:27.251 [main] DEBUG com.multiThreads.test10.TestReentrantLock - execute method2
19:29:27.251 [main] DEBUG com.multiThreads.test10.TestReentrantLock - execute method3
12.2、可打断
public class TestReentrantLock1 {
private static final Logger LOGGER = LoggerFactory.getLogger(TestReentrantLock1.class);
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
// 如果没有竞争那么此方法就会获取lock对象锁
// 如果有竞争就进去阻塞队列,可以被其他线程用interrupt()方法打断
LOGGER.debug("尝试获得锁...");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
LOGGER.debug("没有获得锁");
return;
}
try {
LOGGER.debug("获取到锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
Thread.sleep(1000);
LOGGER.debug("打断 t1...");
t1.interrupt();
}
}
结果:
19:39:49.609 [t1] DEBUG com.multiThreads.test10.TestReentrantLock1 - 尝试获得锁...
19:39:50.587 [main] DEBUG com.multiThreads.test10.TestReentrantLock1 - 打断 t1...
19:39:50.588 [t1] DEBUG com.multiThreads.test10.TestReentrantLock1 - 没有获得锁
12.3、锁超时
立刻失败。
public class TestReentrantLock3 {
private static final Logger LOGGER = LoggerFactory.getLogger(TestReentrantLock3.class);
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
LOGGER.debug("启动...");
// 尝试获得锁,如果成功就获得锁;如果失败就不进入阻塞队列等待
// 另一种方式:lock.tryLock(2, TimeUnit.SECONDS),表示尝试等多久,如2秒内没后获得锁就返回false
if (!lock.tryLock()) { // 这里是立刻返回获取结果
LOGGER.debug("获取立刻失败,返回");
return;
}
try {
LOGGER.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
}
}
结果:
19:54:15.046 [t1] DEBUG com.multiThreads.test10.TestReentrantLock3 - 启动...
19:54:15.061 [t1] DEBUG com.multiThreads.test10.TestReentrantLock3 - 获取立刻失败,返回
12.3.1、锁超时解决哲学家就餐问题
public class TestPhilosopher {
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();
}
}
/**
* 哲学家就餐问题
*/
class Philosopher extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Philosopher.class);
private Chopstick left;
private 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();
}
}
}
}
private void eat() {
LOGGER.debug("eating...");
sleep0(1);
}
private void sleep0(double time) {
try {
Thread.sleep((long)(time * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + "}";
}
}
结果:
18:45:16.394 [苏格拉底] DEBUG com.multiThreads.test8.Philosopher - eating...
18:45:16.394 [亚里士多德] DEBUG com.multiThreads.test8.Philosopher - eating...
18:45:17.405 [阿基米德] DEBUG com.multiThreads.test8.Philosopher - eating...
18:45:17.409 [柏拉图] DEBUG com.multiThreads.test8.Philosopher - eating...
...
12.4、公平锁
ReentranLock默认是不公平的。
12.5、条件变量
synchronized中也有条件变量,就是我们讲原理时那个waitSet休息室,当条件不满足时进入waitSet等待。ReentranLock的条件变量比synchronized强大之处在于,它是支持多个条件变量的,这就好比:
- synchronized是那些不满足条件的线程都在一间休息室等消息;
- 而ReentranLock支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒。
使用流程:
- await前需要获得锁;
- await执行后,会释放锁,进入conditionObject;
- await的线程被唤醒(或打断、或超时)去重新竞争lock锁;
- 竞争lock锁成功后,从await后继续执行。
public class Test1 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// 创建2个新的条件变量(休息室)
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
// 先获得锁
lock.lock();
// 进入休息室等待,await()执行后,会释放锁
condition1.await();
condition1.signal(); // 唤醒条件变量condition1中的某一个线程
condition1.signalAll(); // 唤醒条件变量condition2中的某一个线程
}
}
public class TestCondition {
static final Logger LOGGER = LoggerFactory.getLogger(TestCondition.class);
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) throws InterruptedException {
new Thread(()->{
ROOM.lock();
try {
LOGGER.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
LOGGER.debug("没烟,先歇会!");
waitCigaretteSet.await();
}
LOGGER.debug("可以开始干活了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
ROOM.unlock();
}
},"小南").start();
new Thread(()->{
ROOM.lock();
try {
LOGGER.debug("有外卖没?[{}]", hasTakeout);
while (!hasTakeout) {
LOGGER.debug("没烟,先歇会!");
waitTakeoutSet.await();
}
LOGGER.debug("可以开始干活了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
ROOM.unlock();
}
},"小女").start();
// 模拟送烟和送外卖的线程
Thread.sleep(1000);
new Thread(()->{
ROOM.lock();
try {
hasCigarette = true;
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
},"送烟的").start();
Thread.sleep(1000);
new Thread(()->{
ROOM.lock();
try {
hasTakeout = true;
waitTakeoutSet.signal();
} finally {
ROOM.unlock();
}
},"送外卖的").start();
}
}
结果;
19:26:24.433 [小南] DEBUG com.multiThreads.test11.TestCondition - 有烟没?[false]
19:26:24.438 [小南] DEBUG com.multiThreads.test11.TestCondition - 没烟,先歇会!
19:26:24.438 [小女] DEBUG com.multiThreads.test11.TestCondition - 有外卖没?[false]
19:26:24.438 [小女] DEBUG com.multiThreads.test11.TestCondition - 没烟,先歇会!
19:26:25.434 [小南] DEBUG com.multiThreads.test11.TestCondition - 可以开始干活了
19:26:26.436 [小女] DEBUG com.multiThreads.test11.TestCondition - 可以开始干活了
13、同步模式之顺序控制
13.1、固定运行顺序
比如,必须先2后1打印。
public class Test1 {
private static final Logger LOGGER = LoggerFactory.getLogger(Test1.class);
static final Object lock = new Object();
static boolean t2Runned = false; // 表示t2是否运行过
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized (lock) {
while (!t2Runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
LOGGER.debug("1");
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (lock) {
LOGGER.debug("2");
t2Runned = true;
lock.notify();
}
},"t2");
t1.start();
t2.start();
}
}
结果:
19:47:19.615 [t2] DEBUG com.multiThreads.test12.Test1 - 2
19:47:19.615 [t1] DEBUG com.multiThreads.test12.Test1 - 1
public class Test2 {
private static final Logger LOGGER = LoggerFactory.getLogger(Test2.class);
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();
LOGGER.debug("1");
}, "t1");
t1.start();
Thread t2 = new Thread(()->{
LOGGER.debug("2");
LockSupport.unpark(t1);
},"t2");
t2.start();
}
}
结果:
19:51:17.644 [t2] DEBUG com.multiThreads.test12.Test2 - 2
19:51:17.647 [t1] DEBUG com.multiThreads.test12.Test2 - 1
13.2、交替输出
线程1输出“a”5次,线程2输出“b”5次,线程3输出“c”5次。现在要求输出abcabcabc怎么实现。
13.2.1、wait & notify 版
public class Test {
public static void main(String[] args) {
WaitNotify waitNotify = new WaitNotify(1, 3);
new Thread(()->{
waitNotify.print(1, 2, "a");
}).start();
new Thread(()->{
waitNotify.print(2, 3, "b");
}).start();
new Thread(()->{
waitNotify.print(3, 1, "c");
}).start();
}
}
/**
* 输出内容 等待标记 下一个标记
* a 1 2
* b 2 3
* c 3 1
*/
class WaitNotify {
private static final Logger LOGGER = LoggerFactory.getLogger(WaitNotify.class);
// 等待标记
private int flag;
// 循环次数
private int loopNumber;
public WaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
// 打印
public void print(int waitFlag, int nextFlag, String str) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while(this.flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
LOGGER.debug(str);
flag = nextFlag;
this.notifyAll();
}
}
}
}
结果:
18:45:28.340 [Thread-0] DEBUG com.multiThreads.test12.WaitNotify - a
18:45:28.340 [Thread-1] DEBUG com.multiThreads.test12.WaitNotify - b
18:45:28.340 [Thread-2] DEBUG com.multiThreads.test12.WaitNotify - c
18:45:28.340 [Thread-0] DEBUG com.multiThreads.test12.WaitNotify - a
18:45:28.340 [Thread-1] DEBUG com.multiThreads.test12.WaitNotify - b
18:45:28.340 [Thread-2] DEBUG com.multiThreads.test12.WaitNotify - c
18:45:28.340 [Thread-0] DEBUG com.multiThreads.test12.WaitNotify - a
18:45:28.340 [Thread-1] DEBUG com.multiThreads.test12.WaitNotify - b
18:45:28.340 [Thread-2] DEBUG com.multiThreads.test12.WaitNotify - c
13.2.2、await & signal 版(ReentranLock)
public class Test3 {
private static final Logger LOGGER = LoggerFactory.getLogger(Test3.class);
public static void main(String[] args) throws InterruptedException {
AwaitSignal awaitSignal = new AwaitSignal(3);
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 {
LOGGER.debug("开始...");
a.signal();
} finally {
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock {
private static final Logger LOGGER = LoggerFactory.getLogger(AwaitSignal.class);
// 循环次数
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
/**
* 打印内容
* @param str:要打印的内容
* @param current:进入哪一间休息室
* @param next:下一间休息室
*/
public void print(String str, Condition current, Condition next) {
for (int i = 0; i < loopNumber; i++) {
lock();
try {
current.await();
LOGGER.debug(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock();
}
}
}
}
结果:
18:59:14.282 [main] DEBUG com.multiThreads.test12.Test3 - 开始...
18:59:14.282 [Thread-0] DEBUG com.multiThreads.test12.AwaitSignal - a
18:59:14.282 [Thread-1] DEBUG com.multiThreads.test12.AwaitSignal - b
18:59:14.282 [Thread-2] DEBUG com.multiThreads.test12.AwaitSignal - c
18:59:14.282 [Thread-0] DEBUG com.multiThreads.test12.AwaitSignal - a
18:59:14.282 [Thread-1] DEBUG com.multiThreads.test12.AwaitSignal - b
18:59:14.282 [Thread-2] DEBUG com.multiThreads.test12.AwaitSignal - c
18:59:14.282 [Thread-0] DEBUG com.multiThreads.test12.AwaitSignal - a
18:59:14.282 [Thread-1] DEBUG com.multiThreads.test12.AwaitSignal - b
18:59:14.282 [Thread-2] DEBUG com.multiThreads.test12.AwaitSignal - c
12.2.3、park & unpark版
public class Test4 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
ParkUnpark parkUnpark = new ParkUnpark(3);
t1 = new Thread(()->{
parkUnpark.print("a", t2);
});
t2 = new Thread(()->{
parkUnpark.print("b", t3);
});
t3 = new Thread(()->{
parkUnpark.print("c", t1);
});
t1.start();
t2.start();
t3.start();
// 主线程发起
LockSupport.unpark(t1);
}
}
class ParkUnpark {
private static final Logger LOGGER = LoggerFactory.getLogger(ParkUnpark.class);
// 循环次数
private int loopNumber;
public ParkUnpark(int loopNumber) {
this.loopNumber = loopNumber;
}
public void print(String str, Thread nextThread) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
LOGGER.debug(str);
LockSupport.unpark(nextThread);
}
}
}
结果:
19:07:54.031 [Thread-0] DEBUG com.multiThreads.test12.ParkUnpark - a
19:07:54.033 [Thread-1] DEBUG com.multiThreads.test12.ParkUnpark - b
19:07:54.033 [Thread-2] DEBUG com.multiThreads.test12.ParkUnpark - c
19:07:54.033 [Thread-0] DEBUG com.multiThreads.test12.ParkUnpark - a
19:07:54.033 [Thread-1] DEBUG com.multiThreads.test12.ParkUnpark - b
19:07:54.034 [Thread-2] DEBUG com.multiThreads.test12.ParkUnpark - c
19:07:54.034 [Thread-0] DEBUG com.multiThreads.test12.ParkUnpark - a
19:07:54.034 [Thread-1] DEBUG com.multiThreads.test12.ParkUnpark - b
19:07:54.034 [Thread-2] DEBUG com.multiThreads.test12.ParkUnpark - c