线程协作-通信
什么是线程协作/线程通信
线程之间通过某种方式传递信号或消息,达到互相协作的目的,称为线程通信/线程协作。Java中可以使用object.wait(),object.notify()/object.notify()组合使用或使用JDK1.5之后的Lock接口的方法,作为线程通信的实现。
模版代码
等待/通知机制有一套模版代码可以直接使用,先看代码,后面会解释模版代码为什么这样实现
wait()
//用同步块包裹wait()逻辑
synchronized (monitor) {
while (!flag) {//当条件不成立时,线程进入等待
wait();
}
logic();//当线程被唤醒并且条件成立时,执行逻辑代码
}
notify()
//用同步块包裹notify()逻辑
synchronized (this) {
flag = true;//设置条件成立
notify();//通知等待的线程
}
问题
下面通过几个问题,来试验一下wait()/notify()的特性
1. 为什么wait()/notify()/notifyAll()必须在synchronized同步块中
保证原子性。
等待/通知的场景中有“条件”这个变量,这个变量的设置操作是在通知线程中执行;读取是在等待线程中执行,显然“设置+通知”和“读取+等待”必须是原子性的,否则变量的操作和等待通知模式就失去了意义。所以synchronized同步块的作用是为了保证操作的原子性。
2. 为什么wait()放在while循环里,放在if里行不行
不行。
假设使用if:
- 第一次条件不满足时,a线程进入等待;
- 然后另一个线程b执行设置条件为true,并调用notify()方法;
- 这时a线程收到信号,结束等待,执行logic
如果只有两个线程修改条件变量,那么if是可行的;但如果2、3步之间,有另一个线程将条件变量设置为false,那么a线程结束等待执行logic就是错误的。
所以为了确认等待线程被唤醒之后,是满足条件的,必须将条件变量的判断放在while循环中。
3. wait()方法会释放锁么,为什么
会释放与该wait()方法所属对象的内部锁,其他对象的内部锁或显式锁不会释放。
调用wait()方法的线程是等待在某个对象上的,锁也是作用在某个对象上的。
调用wait()方法之后,会释放该对象上的锁,以便其他线程能获得锁,执行notify()方法;如果wait()方法不释放锁,其他线程尝试获取锁时就会造成死锁。
4. 等待线程被唤醒的时候需要重新获取锁么
需要。
等待线程被唤醒后,需要重新持有同步块的锁才能进入临界区继续执行。如果一直获取不到锁,就会一直处于等待状态,导致的现象是:虽然已经有线程修改条件变量并唤醒等待线程,但等待线程一直没有执行。
5. notify()方法会释放锁么
不会。
notify()方法执行完毕不会立即释放锁,锁会在synchronized同步块执行完成后释放,所以notify()方法要尽量放在同步块的最后。防止唤醒了等待线程,但等待线程又阻塞在了锁上,导致不必要的上下文切换。
6. notify()与notifyAll()的区别
假设有三个线程都处于等待状态,唤醒线程如果执行一次notify()方法,只有一个线程会被唤醒并执行逻辑;唤醒线程如果执行一次notifyAll()方法,所有的等待线程都会唤醒并竞争锁资源,没有得到锁资源的线程会阻塞在这个锁上等着其他的等待线程释放锁,最终所有三个等待线程都会唤醒并执行逻辑。
内部实现
monitor对象内部会维护两个队列:
- 入口集Entry Set(锁池),用于存放申请该对象内部锁的线程;
- 等待集Wait Set(等待池),用于存放等待在这个对象上的线程;
object.wait()方法的伪代码实现如下:
package signal.synchronize;
import java.util.Collection;
/**
* object.wait()方法的伪代码实现
* <p>2020/8/2 18:52</p>
*
* @author konglinghan
* @version 1.0
*/
public class PseudoWait {
private Collection<Thread> entrySet;//klh 锁池
private Collection<Thread> waitSet;//klh 等待池
public void myWait() {
//klh wait方法必须在同步块内持有锁,否则直接抛异常
if (!entrySet.contains(Thread.currentThread())) {
throw new IllegalMonitorStateException();
}
//klh 加入等待池
if (!waitSet.contains(Thread.currentThread())) {
waitSet.add(Thread.currentThread());
}
releaseLock(this);//klh 释放锁
pause(Thread.currentThread());//klh 暂停当前线程,等待唤醒<1>
acquireLock(this);//klh 被唤醒,重新申请锁资源<2>
waitSet.remove(Thread.currentThread());//klh 从等待集中移除
return;//klh wait()方法返回
}
/**
* 暂停线程
*
* @param thread
*/
private void pause(Thread thread) {
}
/**
* 申请内部锁
*
* @param monitor 锁所在的对象
*/
private void acquireLock(Object monitor) {
if (entrySet.isEmpty()) {
return;
} else {
//klh 如果没有获取到锁,线程阻塞,进入锁池等待锁资源
entrySet.add(Thread.currentThread());
}
}
/**
* 释放锁
*
* @param monitor 锁所在的对象
*/
private void releaseLock(Object monitor) {
}
}
客户代码中,执行完object.wait()方法后,线程在<1>处暂停;被其他线程唤醒后,在<2>处接着执行。可以看到被唤醒之后,第一步就是要重新获取内部锁,获取到锁资源之后才能继续执行object.wait()方法。
现在我们重新来看第六个问题:notify()与notifyAll()的区别
notify会将等待池中的一个线程唤醒,这个等待线程正常的话会直接拿到锁,从等待集中移出,继续执行逻辑代码;notifyAll()会将等待池中所有的线程都唤醒,这些线程竞争同一把锁,没有竞争到锁的线程进入锁池阻塞,上一个线程释放锁后会有另一个线程竞争到锁,并从锁池中移出,然后从等待池中移出。最终所有被唤醒的线程都会从锁池和等待池中移出,执行逻辑代码。