1. 基本概念
在业务处理过程中,线程间常常需要进行通信,从而协同完成某个事项。比如,有两个线程A和B,线程A负责读取数据,线程B负责处理数据,线程A在读取完数据后必须通知线程B进行数据处理,这个“通知”就是线程通信的表现。
2. 忙等待
怎样实现线程通信呢?可以通过在线程间访问一个共同的资源(标志位)来实现。比如线程A和线程B,线程A负责读取数据,线程B负责处理数据。我们在A和B之间设置一个共同的资源:布尔值的Flag,标记A有没有读取完数据(默认为false,表示未读取完数据)。我们让B一直监听着这个Flag标记(使用while循环),当A读取完数据后,会将这个Flag设为true,这时B监听到Flag变为true了,就可以进行数据处理操作了。
简要代码如下:
public class SignalBusyWait {
private volatile static Boolean flag = false;//共享资源,设置标志位
public static void main(String[] args) {
Thread A = new Thread(new Runnable() {//线程A,负责读取数据,读取完设置flag为true
@Override
public void run() {
// TODO Auto-generated method stub
try {
System.out.println("读取数据");
Thread.sleep(2000);
flag = true;
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
},"Thread A");
Thread B = new Thread(new Runnable() {//线程B,负责处理数据,死循环监听flag
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
if(flag){
System.out.println("处理数据");
break;//处理完数据,退出死循环
}else{
System.out.println("flag 为false,表示数据未读取完,B线程进行等待");
}
}
}
},"Thread B");
B.start();
A.start();
}
}
以上方式虽然能够实现线程通信,但是B线程不断地监听标志位,一直处理“等待”状态,这就是忙等待现象。那么如何处理忙等待现象呢?Java提供了notify和wait机制。
3. notify,wait和notifyAll
Java给所有的对象都内置了三个方法,用于线程通信。分别是notify,wait和notifyAll(这三个方法都是Object类的方法,Object类是所有Java类的父类,等于说所有Java对象都拥有了这三个方法)。
- 同一把锁 : notify和wait方法必须使用同一监听对象(即竞争同一把锁)。
- 就绪队列和等待队列:同一个监听对象上有两个队列,分别是就绪队列和等待队列。当线程不满足执行条件时(比如负责处理数据的B线程得到锁后,发现数据还没有读取完,无法进行处理),就会进入等待队列,等待执行条件满足时被唤醒。当线程被唤醒后并不是马上执行,而是进入就绪队列,随时准备好竞争锁,获得锁后才能真正执行。
- wait:当线程获得锁后发现执行条件不满足,这时候会调用wait方法,将这个线程加入到等待队列中,并释放锁,让其他就绪的线程可以再次竞争锁。
- notify:当线程中调用notify方法时,当前线程并不会获放锁,当前线程会继续执行下去。它做的只是唤醒“等待队列”中的一个线程,可以进入“就绪队列”了。
-
notifyAll: 与notify相同,只是它唤醒的是整个等待队列中的所有线程。
上面的代码利用wait和notify实现如下:
public class NotifyWait {
public final static Object monitorObject = new Object();//监听对象
public static void main(String[] args) {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
synchronized(monitorObject){
try {
Thread.sleep(2000);
System.out.println("读取数据");
monitorObject.notify();//读取完数据,通知线程B处理数据
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
},"Thread A");
Thread B = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
synchronized(monitorObject){
try {
System.out.println("B线程获得锁,但发现数据未读取完,进行等待");
monitorObject.wait();
Thread.sleep(2000);
System.out.println("处理数据");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
},"Thread B");
B.start();
A.start();
}
}
4. 使用wait和notify需要注意的问题
4.1 必须与synchronized关键字联用
使用wait和notify进行通信时,必须与synchronized关键字结合使用,即必须在synchronized块中调用wait和notify方法,才能够保证同一时间只有一个线程发送信号。
4.2 信号丢失问题
有一种情况,当对象调用notify方法时,等待队列中没有线程,则没有任何线程会收到这个信号,之后调用wait方法的线程将会一直等待,因为唤醒它的信号已经丢失了。怎么办呢,我们可以定义一个成员变量用来保存信号的状态,然后在调用notify方法和wait方法前得到这个成员变量的值,判断信号是否已经发出,如以下代码所示:
public class MyWaitNotify2{
MonitorObject myMonitorObject = new MonitorObject();
boolean wasSignalled = false;
public void doWait(){
synchronized(myMonitorObject){
if(!wasSignalled){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
//clear signal and continue running.
wasSignalled = false;
}
}
public void doNotify(){
synchronized(myMonitorObject){
wasSignalled = true;
myMonitorObject.notify();
}
}
}
我们定义了wasSingalled成员变量用来标示信号是否发出,这从一定程度上保证了信号不会丢失。
4.3 伪唤醒问题
上例中,有时候用于标示信号是否发出的标志位在监听对象并未调用notify方法时,就被修改成了true,从而导致本不该唤醒的进程被唤醒,唤醒的线程会继续执行后面的代码,也就是所谓伪唤醒问题。我们可以将对标志位的判断由if改为while,让被唤醒的线程再判断一次标志位,确定是否唤醒了正确的线程,从而避免伪唤醒问题。
4.4 不能使用字符串常量作为监听对象
注意,不能使用字符串常量作为监听对象,因为在JVM中是将字符串常量编译到同一个对象的。举个例子,当有两个对象A和B都使用字符串常量(无论是否同一常量)作为监听对象,当对象A调用notify方法时,它可能会唤醒对象B等待队列中的线程,从而造成混乱。