线程通信
线程通信指的是多个线程在运行的期间,相互之间的数据交互协作。
1.通信方式
实现多个线程直接的协作,涉及到的通信方式主要四类。
1)文件共享
2)网络共享
3)共享变量
4)JDK提供的线程协调API
1.文件共享
线程A写文件,线程B读取文件达到线程协作。
2.网络共享
线程A发送数据,线程B接受数据达到线程协作。
3.共享变量
利用内存的公共区域,共享变量。线程A修改变量,线程B读取变量达到线程协作。
以上3种方式都是比较触及的,我们主要链接JDK给我提供的API
2.线程协作 JDK API
生产者消费者模型是线程协作的典型场景(线程阻塞、线程唤醒),我们以一个示例去理解
示例:线程A提车,没有车,则不执行。线程B有车了,通知线程A继续执行
2.1 suspend/resume
suspend挂起线程,resume恢复线程执行。这两个API是Thread提供的。
示例:
public static Object benz = null;
public void suspendResumeTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (benz == null) { // 如果没奔驰,则进入等待
System.out.println("1、进入等待");
Thread.currentThread().suspend();
}
System.out.println("2、提到车,回家");
});
consumerThread.start();
// 3秒之后,拉来一辆奔驰
Thread.sleep(3000L);
benz = new Object();
consumerThread.resume();
System.out.println("3、通知消费者");
}
suspend/resume虽然是Thread提供的,因为很容易写出死锁的代码。 所以被弃用了。
死锁示例1:在写同步代码的时候容易出现:suspend在挂起之后并不会释放锁
public void suspendResumeDeadLockTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (benz == null) { // 如果没奔驰,则进入等待
System.out.println("1、进入等待");
// 当前线程拿到锁,然后挂起
synchronized (this) {
Thread.currentThread().suspend();
}
}
System.out.println("2、提到车,回家");
});
consumerThread.start();
// 3秒之后,拉来一辆奔驰
Thread.sleep(3000L);
benz = new Object();
// 争取到锁以后,再恢复consumerThread
synchronized (this) {
consumerThread.resume();
}
System.out.println("3、通知消费者");
}
这种情况下消费者如果拿到锁,消费者就挂起了,生产者要通知消费者必须要抢到锁,但是因为消费者抢到锁之后挂起,并没有释放锁,生产者是抢不到锁,这就死锁了。
死锁示例2: API调用顺序:suspend在resume后执行死锁
public void suspendResumeDeadLockTest2() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (benz == null) {
System.out.println("1、没奔驰,进入等待");
try { // 为这个线程加上一点延时
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这里的挂起执行在resume后面
Thread.currentThread().suspend();
}
System.out.println("2、提到车,回家");
});
consumerThread.start();
// 3秒之后,拉来一辆奔驰
Thread.sleep(3000L);
benz = new Object();
consumerThread.resume();
System.out.println("3、通知消费者");
consumerThread.join();
}
这种情况下suspend在resume后执行,线程恢复不了执行,死锁了。
2.2 wait/notify
这一对API只能由统一对象锁的持有者调用,也就是写在同步代码块里面,否则抛异常。
wait使当前线程等待,加入该对象的等待池,并释放锁。
notify/notifyAll唤醒一个或所有正在等待这个对象的线程
因为必须用在同步代码块里面, wait/notify针对锁的问题不存在了。但是顺序需要注意。
示例:
public void waitNotifyTest() throws Exception {
// 启动线程
new Thread(() -> {
if (benz == null) { // 如果没奔驰,则进入等待
synchronized (this) {
try {
System.out.println("1、进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、提到车,回家");
}).start();
// 3秒之后,拉来一辆奔驰
Thread.sleep(3000L);
benz = new Object();
synchronized (this) {
this.notifyAll();
System.out.println("3、通知消费者");
}
}
注意:虽然wait会释放锁,但是对顺序有要求,如果wait在notify之调用,线程就永远处于WAITING状态了。
死锁示例:
public void waitNotifyDeadLockTest() throws Exception {
// 启动线程
new Thread(() -> {
if (benz == null) { // 如果没奔驰,则进入等待
try {
Thread.sleep(5000L);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
synchronized (this) {
try {
System.out.println("1、进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、提到车,回家");
}).start();
// 3秒之后,拉来一辆奔驰
Thread.sleep(3000L);
benz = new Object();
synchronized (this) {
this.notifyAll();
System.out.println("3、通知消费者");
}
}
这种情况下wait在notify后执行,线程收不到通知,死锁了。
2.3 park/unpark
park等待许可,unpark提供许可令牌,让线程继续执行,park/unpark没有顺序要求。
多次unpark之后调用park,线程会直接执行,因为已经拿到许可令牌了。
许可令牌不会叠加,多次调用park,只有第一次会拿到许可,继续执行,后续的调用则进入等待。
示例:
public void parkUnparkTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (benz == null) { // 如果没奔驰,则进入等待
System.out.println("1、进入等待");
LockSupport.park();
}
System.out.println("2、提到车,回家");
});
consumerThread.start();
// 3秒之后,拉来一辆奔驰
Thread.sleep(3000L);
benz = new Object();
LockSupport.unpark(consumerThread);
System.out.println("3、通知消费者");
}
park/unpark不会释放锁,所以在同步代码块里面使用不当就容易死锁。
死锁示例:
public void parkUnparkDeadLockTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (benz == null) { // 如果没奔驰,则进入等待
System.out.println("1、进入等待");
// 当前线程拿到锁,然后挂起
synchronized (this) {
LockSupport.park();
}
}
System.out.println("2、提到车,回家");
});
consumerThread.start();
// 3秒之后,拉来一辆奔驰
Thread.sleep(3000L);
benz = new Object();
// 争取到锁以后,再恢复consumerThread
synchronized (this) {
LockSupport.unpark(consumerThread);
}
System.out.println("3、通知消费者");
}
消费者挂起之后没有释放锁,生产者永远获取不到锁,死锁。
2.4 join
有人说join也是一种,其实join底层使用的wait/notify
结语
虽然都是些简单例子,但是我们通过这些例子去看正确的操作,还有死锁的情况,我可以在平时写这类代码的时候可以避免踩坑。弃用的suspend/resume就不要用了,wait/notify、park/unpark看场景需要使用。
第一次写博客,排版乱糟糟,有很多知识点还没完全讲到,讲得不够详细,多多谅解。迈出一步,那也是进步。