JAVA多线程11-基础篇-线程间通讯wait,notify

本章介绍 线程间的协作方式,主要包含wait(),notify(),notifyAll()方法的使用以及代码示例

一、等待/通知机制介绍

在多线程环境下,为了保证线程安全,我们使用同步锁来保证任何时刻只有一个线程可以访问共享资源。而在实际场景中,除了线程互斥,我们也希望线程间可以通过协作来解决问题,而java并没有手段使得线程间直接的交互,通常的做法是通过一个共享变量来实现线程间的通讯,例如:A线程需要读取B线程发送的信息,代码如下:

public class Demo1 {
  private static String msg;
  public static void main(String[] args) {
        work();
  }
  public static void work() {
      Thread t1 = new Thread(() -> {
              System.out.println(msg);
      });
      Thread t2 = new Thread(() -> {
              msg = "t2 set the msg";
      });
      t1.start();
      t2.start();
    }
}

结果输出:
null 或者 t2 set the msg

结果说明:这个例子中线程t1,t2通过msg来通信,t1需要读取t2设置的msg,由于线程的执行无法预测,我们不能保证t1一定能读取t2设置的msg,所以会输出如上的结果。

如果我们想保证t1能够读取到t2设置的值,可以调整为如下代码:

public class Demo1 {
    private static String msg;
    public static void main(String[] args) {
        work();
    }
    public static void work() {
        Thread t1 = new Thread(() -> {
            while (msg == null) {
            }
            System.out.println(msg + "!!!");
        });
        Thread t2 = new Thread(() -> {
            msg = "t2 set the msg";
        });
        t1.start();
        t2.start();
    }
}

结果输出:
t2 set the msg!!!

结果说明:t1通过死循环的方式直到msg有值为止,这种方式虽然能满足需求,但是非常耗CPU资源。

为了更好的实现线程间的交互,java提供了等待/通知机制,本质上也是通过锁来实现线程间的通信。这种机制是通过Object类的wait(),notify()方法来实现,下面对这几个方法进行介绍。

二、wait,notify,notifyAll方法介绍

/**
* Causes the current thread to wait until another thread invokes the
* {@link java.lang.Object#notify()} method or the
* {@link java.lang.Object#notifyAll()} method for this object.
* In other words, this method behaves exactly as if it simply
* performs the call {@code wait(0)}.
* <p>
* The current thread must own this object's monitor. The thread
* releases ownership of this monitor and waits until another thread
* notifies threads waiting on this object's monitor to wake up
* either through a call to the {@code notify} method or the
* {@code notifyAll} method. The thread then waits until it can
* re-obtain ownership of the monitor and resumes execution.
* <p>
* As in the one argument version, interrupts and spurious wakeups are
* possible, and this method should always be used in a loop:
* <pre>
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait();
* ... // Perform action appropriate to condition
* }
* </pre>
* This method should only be called by a thread that is the owner
* of this object's monitor. See the {@code notify} method for a
* description of the ways in which a thread can become the owner of
* a monitor.
*/
public final native void wait() throws InterruptedException;

/**
* Wakes up a single thread that is waiting on this object's
* monitor. If any threads are waiting on this object, one of them
* is chosen to be awakened. The choice is arbitrary and occurs at
* the discretion of the implementation. A thread waits on an object's
* monitor by calling one of the {@code wait} methods.
* <p>
* The awakened thread will not be able to proceed until the current
* thread relinquishes the lock on this object. The awakened thread will
* compete in the usual manner with any other threads that might be
* actively competing to synchronize on this object; for example, the
* awakened thread enjoys no reliable privilege or disadvantage in being
* the next thread to lock this object.
* <p>
* This method should only be called by a thread that is the owner
* of this object's monitor. A thread becomes the owner of the
* object's monitor in one of three ways:
* <ul>
* <li>By executing a synchronized instance method of that object.
* <li>By executing the body of a {@code synchronized} statement
* that synchronizes on the object.
* <li>For objects of type {@code Class,} by executing a
* synchronized static method of that class.
* </ul>
* <p>
* Only one thread at a time can own an object's monitor.
*/
public final native void notify();

/**
* Wakes up all threads that are waiting on this object's monitor. A
* thread waits on an object's monitor by calling one of the
* {@code wait} methods.
* <p>
* The awakened threads will not be able to proceed until the current
* thread relinquishes the lock on this object. The awakened threads
* will compete in the usual manner with any other threads that might
* be actively competing to synchronize on this object; for example,
* the awakened threads enjoy no reliable privilege or disadvantage in
* being the next thread to lock this object.
* <p>
* This method should only be called by a thread that is the owner
* of this object's monitor. See the {@code notify} method for a
* description of the ways in which a thread can become the owner of
* a monitor.
*/
public final native void notifyAll();
wait,notify,notifyAll方法总结:
  • 这几个方法都定义在Object类中
  • 都是final方法,不能被重写
  • wait是阻塞方法,线程被中断会抛出InterruptedException异常。当前线程必须拥有对象锁,才能调用对象的wait方法,调用wait方法会使当前线程阻塞,当前线程会进入到对象的等待队列,同时释放对象锁,调用wait的伪代码如下:
synchronized (obj) {
while (condition does not hold)
obj.wait();
... // do something
}

wait(timeout) 在wait()的基础上增加了超时时间,超过该时间线程会被重新唤醒
  • 调用notify、notifyAll方法的线程必须拥有对象锁,notify会唤醒等待对象监视器的单个线程,被唤醒的线程是随机的,notifyAll方法唤醒等待对象监视器的所有线程,被唤醒的线程(等待线程)不会立即竞争对象锁,只有当前的唤醒线程释放对象锁后,等待线程才能竞争对象锁

三、代码示例

先描述一个场景:
外婆家餐厅一直很火爆,我去过杭州西湖、北京APM、沈阳万象城这三家,味道都差不多,不过西湖边上的新白鹿感觉更胜一筹啊_,扯远了....
我们去外婆家吃饭,如果餐厅有空位,我们可以直接进去用餐,如果餐厅满员,我们需要排队,我们等着叫"XXX号,外婆让你回家吃饭了",然后我们就可以进入餐厅吃饭,餐厅的容量实际是消费者和服务员之间的共享资源

这个场景抽象一下,可以认为用户是一个线程,而餐厅的服务人员相当于另外一个线程,餐厅是用餐者和服务人员的共享信息
代码如下:

public class WaitNotifyDemo1 {
    public static void main(String[] args) {
        final Queue queue = new LinkedList<>();
        Thread producer = new Producer("producer", queue);
        Thread consumer = new Consumer("consumer", queue);
        producer.start();
        consumer.start();
    }
}

public class Producer extends Thread {

    private final Queue sharedQueue;//餐厅容量
    public Producer(String name, Queue queue) {
        super(name);
        this.sharedQueue = queue;
    }

    @Override
    public void run() {
        //假设餐厅最多容纳10桌客人
        for (int i = 0; i <= 10; i++) {
            synchronized (sharedQueue) {
                while (sharedQueue.size() == 10) {
                    try {
                        sharedQueue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                sharedQueue.add(i);
                System.out.println("producer " + i);
                sharedQueue.notify();
            }
        }
    }
}

public class Consumer extends Thread {
    private final Queue sharedQueue;
    public Consumer(String name, Queue queue) {
        super(name);
        this.sharedQueue = queue;
    }

    @Override
    public void run() {
        //消费者持续不断的来就餐
        while (true) {
            synchronized (sharedQueue) {
                while (sharedQueue.size() == 0) {
                    try {
                        sharedQueue.wait();
                        sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Integer o = (Integer) sharedQueue.poll();
                sharedQueue.notify();
                if (o == 10) {//只接纳10桌客人
                    System.out.println("stop service");
                    break;
                }
            }
        }
    }
}
结果输出:
producer 0
producer 1
producer 2
producer 3
producer 4
producer 5
producer 6
producer 7
producer 8
producer 9
producer 10
stop service

结果分析:

  1. 生产者和消费者共享一个队列Queue
  2. 核心流程都是先获取对象锁,然后根据条件判断是等待还是唤醒

四、几个问题

(1) 为什么调用wait,notify 方法都必须获得对像锁?
wait方法使当前线程挂起,notify会唤醒在该对象上挂起的线程,也就是说wait,notify一定是成对出现且针对同一个对象的挂起、唤醒操作,我们知道每个对象都有一个监视器对象,synchronized就是通过进入监视器来实现线程间互斥的,基于这个特性,恰好能使wait,notify建立关系,形成协作,所以通过对象锁,既能实现线程间互斥,也能实现线程间协作

(2)为什么 wait,notify,notifyAll都定义在Object类中?
因为wait,notify是依赖 同步锁进行协作的,而每个对象都有且仅有一把对象锁,所以定义在Object类中可以完美解决线程间协作的问题

(3)如果不在同步块或者同步方法中执行wait,notify会怎样?
大家可以自行验证一下,这里就不做实践了

五、总结

(1)等待/通知 机制是通过对象锁来实现的
(2)notify,notifyAll,wait必须在同步块或者同步方法中执行
(3)线程间的交互模型如图所示:


转载请注明作者及出处,并附上链接http://www.jianshu.com/u/ada8c4ee308b

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

推荐阅读更多精彩内容