一、锁池和等待池
1)锁池:存放竞争锁失败的线程;
假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
2)等待池: 调用wait()的线程,释放锁之后,将当前线程加入等待池;
假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中。
二、线程间通信
每个线程都拥有自己独立的栈空间,从线程被启动到终止,如果每个运行中的线程都是独立的运行那只能带来很少的价值,我们需要多个线程彼此之间相互协作的来完成工作,我们可以用通知/等待机制来实现线程间通信。
三、等待/通知机制
等待和通知的相关方法是任意Java对象都具备的,这些方法被定义在所有方法的超类java.lang.Object上;
方法名称 | 描述 |
---|---|
wait() | 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()后,会释放对象的锁 |
wait(long) | 超时等待一段时间,这里的参数是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回 |
wait(long,int) | 对于超时时间更细粒度的控制,可以达到纳秒 |
notify() | 通知任意一个在该对象上等待的线程,使其从wait()方法返回,而返回的潜艇是该线程获取到了对象的锁 |
notifyAll() | 通知所有等待在该对象上的线程 |
等待/通知机制是指一个线程A调用了对象o的wait()方法进入等待状态,而另一个线程B调用了对象o的notify()或者notifyAll()方法,线程A收到通知后从对象o的wait()方法返回,进而执行后续操作。
上述两个线程通过对象o来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
1、注意:
-
调用wait()和notify()的必须是一个对象,调用wait()的对象和加锁对象也必须是同一个对象,否则会抛出异常;
-
wait()、notify()和notifyAll()必须要在锁中使用,即使用时需要先对调用对象o加锁;
-
调用wait()后,当前线程会释放锁,使其他线程能够继续获取锁使用,当前线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列;
-
notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifAll()的线程释放锁之后,等待线程才有机会从wait()返回;
-
notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED;
-
从wait()方法返回的前提是获得了调用对象的锁;
2、从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从
wait()方法返回时能够感知到通知线程对变量做出的修改;
上面的图中,WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行。
3、等待/通知的经典范式
该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。
1)等待方遵循如下原则。
- 获取对象的锁。
- 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
- 条件满足则执行对应的逻辑。
对应的伪代码如下:
synchronized(o) {
while(条件不满足) {
o.wait();
}
//对应的处理逻辑
}
2)通知方遵循如下原则。
- 获得对象的锁。
- 改变条件。
- 通知所有等待在对象上的线程。
对应的伪代码如下。
synchronized(o) {
//改变条件
o.notifyAll();
}