Java学习笔记

Java并发编程(四)线程通信

2017-06-27  本文已影响47人  爱迪生玛玛

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对象都拥有了这三个方法)。

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等待队列中的线程,从而造成混乱。

上一篇 下一篇

猜你喜欢

热点阅读