美文网首页Java学习笔记
Java并发编程(四)线程通信

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

作者: 爱迪生玛玛 | 来源:发表于2017-06-27 20:42 被阅读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对象都拥有了这三个方法)。

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

相关文章

  • java内存模型

    java内存模型基础 并发编程,两个关键问题:线程通信和线程同步通信机制:共享内存和消息传递 java并发采用共享...

  • Java并发编程-1

    基本知识一 线程优先级二 线程的状态三 线程函数四 线程间通信 参考:java并发编程的艺术 一 线程优先级 线程...

  • Java内存模型

    2.7 Java内存模型 2.7.1 并发编程模型 在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间...

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

    1. 基本概念 在业务处理过程中,线程间常常需要进行通信,从而协同完成某个事项。比如,有两个线程A和B,线程A负责...

  • 2.Java内存模型

    1.java并发编程的线程间通信及线程间如何同步线程间通信分为:共享内存,消息传递。线程间同步:共享内存是代码指定...

  • java并发编程——同步

    在上一篇 java并发编程——内存模型中我们提到:并发编程中,我们需要处理两个关键问题:线程之间如何通信和线程之间...

  • Java并发教程

    Java并发教程 Java并发概述Java并发环境设置Java并发主要操作Java并发性线程间通信Java并发同步...

  • Java内存模型分析

    在学习Java内存模型之前,先了解一下线程通信机制。 1、线程通信机制 在并发编程中,线程之间相互交换信息就是线程...

  • Java内存模型基础

    Java内存模型 并发编程模型中的两个关键问题 在并发编程中有两个关键的问题: 线程之间如何通信 线程之间如何同步...

  • 线程

    Java 并发编程:线程池的使用 Java 并发编程:线程池的使用java 多线程核心技术梳理 (附源码) 本文对...

网友评论

    本文标题:Java并发编程(四)线程通信

    本文链接:https://www.haomeiwen.com/subject/aqctfxtx.html