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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容