从Guarded Block来看Java中的wait和notify方法

预备知识

Java线程的生命周期

概览

本文探究一下Java最基础的机制之一:线程同步
我们先讨论一些并发相关的术语和方法论,接着会提供一个简单例子来处理并发问题,可以帮助我们更好的理解wait()和notify()方法。

线程同步

多线程环境下,每个线程都可能去修改相同资源,如果线程没有被较好的管理,那就可能会出现并发问题。

多线程之间经常需要协同工作,最常见的方式是使用保护块(Guarded Blocks),它循环检查一个条件(通常初始值为true),直到条件发生变化才跳出循环继续执行。

public void guardedJoy() {
    // Simple loop guard. Wastes processor time. Don't do this!
    while(!joy) {}
    System.out.println("Joy has been achieved!");
}

但是使用Guarded Blocks的方法不停的检查循环条件实际上是一种资源浪费,更加高效的方法是调用Object.wait将当前线程挂起,直到有另一线程发起事件通知(尽管通知的事件不一定是当前线程等待的事件)。

public synchronized void guardedJoy() {
    // This guard only loops once for each special event, 
    // which may not be the event we're waiting for.
    while(!joy) {
        try {
            wait();
        } catch (InterruptedException e) {}
    }
    System.out.println("Joy and efficiency have been achieved!");
}

我们今天要讨论的就是wait()和notify()方法:

  • Object.wait() – 挂起线程,
  • Object.notify() – 唤醒线程

补充:
下面这张图是wait()和notify()在线程的生命周期作用域的图解:

线程的生命周期

可以看到有很多种方式可以控制生命周期,本文我们只关注wait()和notify()方法。

wait()方法

/**
 * 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 (&lt;condition does not hold&gt;)
 *             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.
 */ 

当一个线程调用wait方法时,它释放锁并挂起。然后另一个线程请求并获得这个锁并调用Object.notifyAll()通知所有等待该锁的线程(之后当前线程释放该锁),此时第一个线程收到通知获取到该锁,从wait()方法返回并继续执行。

wait()方法有三个重载方法:

wait()

wait方法会使当前线程无限期等待,直到另一个线程调用了当前对象的notify()或notifyAll()方法。

wait(long timeout)

  • 调用该方法可指定一段时间的限期等待,之后会由系统自动唤醒该线程。
  • 在未到达timeout时间前也可通过当前对象的notify()或notifyAll()方法唤醒。

wait(long timeout, int nanos)

这是另一个限期等待的重载方法,不同的是提供了更高精度的timeout。

notify() & notifyAll()

notify()方法被用来唤醒在等待对象的内置锁的线程,有两种唤醒方式:

notify()

/**
 * 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.
 */ 

它只会唤醒一个线程。但由于它并不指定哪一个线程被唤醒,所以一般大量相似任务的多线程环境中使用。因为对于这类任务,我们其实并不关心哪一个线程被唤醒。

对于该方法,当前线程必须拥有当前对象的内置锁或监视器锁(intrinsic lock aks monitor lock),根据Java文档,可通过以下三种方式的任意一种:

  • 在给定对象上执行了同步方法
  • 在给定对象上执行了同步块逻辑
  • 执行给定对象上的同步静态方法

注意某一时间只有一个活跃线程能获取到对象的内置锁

notifyAll()

/**
* 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.
*/

该方法会唤醒所有在该对象上等待内置锁的线程。
被唤醒的线程正常执行下去直到完成任务。
但在允许唤醒的线程开始继续执行逻辑之前,我们通常会定义一个快速检查,以确定继续执行线程所需的条件,因为可能会出现这种被唤醒的线程没收到通知的情况(一个对象多个方法中都调用了wait(),但是notifyAll()可能只针对某个方法有意义)

生产者-消费者同步问题

在我们理解了上述叙述后,我们来看一个简单的生产者-消费者例子:

  • 生产者应该发送一条数据给消费者
  • 如果生产者还未生产完毕,消费者此时不能处理数据
  • 相同的,如果消费者未处理完数据,生产者不能发送下一条数据

我们首先创建一个Drop类,它包含了生产者需要传送给消费者的数据,我们会使用wait()和notifyAll()方法来让两个线程之间共享数据:

public class Drop {
    // Message sent from producer to consumer.
    private String message;
    // True if consumer should wait for producer to send message,
    // false if producer should wait for consumer to retrieve message.
    private boolean empty = true;

    public synchronized String take() {
        // Wait until message is
        // available.
        while (empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = true;
        // Notify producer that
        // status has changed.
        notifyAll();
        return message;
    }

    public synchronized void put(String message) {
        // Wait until message has been retrieved.
        while (!empty) {
            try { 
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = false;
        // Store message.
        this.message = message;
        // Notify consumer that status
        // has changed.
        notifyAll();
    }
}

我们来分解一下:·

  • message变量表示需要被传送的数据
  • 布尔类型的empty变量是生产者和消费者用来做同步使用的:
    • 如果为true,消费者需要等待生产者生产完毕
    • 如果为false,生产者需要等待消费者消费完毕
  • 生产者使用put()方法发送消息给消费者
    • 如果empty为false,调用wait()等待
    • 如果empty为true,设置empty为false,设置message为传入的消息,并调用notifyAll()方法来唤醒其他线程表明有一个事件发生了,大家可以检查一下当前状态看看是否需要继续执行。
  • 相似的,消费者使用take()方法接收消息
    • 如果empty被生产者设置为false,那它就继续执行,否则调用wait()方法等待
    • 当条件满足后(empty为false),设置empty为true,唤醒其他等待线程并返回接收消息

为什么要把wait()方法放入while语句中?

因为线程唤醒后当前方法的循环条件不一定发生了改变。

为什么要同步put()和take()方法?

假设o是用来调用wait的对象,当一个线程调用o.wait(),它必须要拥有o的内部锁(否则会抛出异常),获得d的内部锁的最简单方法是在一个synchronized方法里面调用wait()。

我们现在创建Producer和Consumer。

先看看Producer:

import java.util.Random;

public class Producer implements Runnable {
    private Drop drop;

    public Producer(Drop drop) {
        this.drop = drop;
    }

    public void run() {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };
        Random random = new Random();

        for (int i = 0;
             i < importantInfo.length;
             i++) {
            drop.put(importantInfo[i]);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
        drop.put("DONE");
    }
}

对Producer来说:

  • 我们定义了一个消息数据数组,在循环内一个一个的生产出去
  • 对于每个消息数据,我们只调用put()方法
  • 最后我们休眠一个随机数来模拟耗时的操作

下面是Consumer的实现:

import java.util.Random;

public class Consumer implements Runnable {
    private Drop drop;

    public Consumer(Drop drop) {
        this.drop = drop;
    }

    public void run() {
        Random random = new Random();
        for (String message = drop.take();
             ! message.equals("DONE");
             message = drop.take()) {
            System.out.format("MESSAGE RECEIVED: %s%n", message);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
    }
}

实现很简单,就是在for循环中调用drop.take()方法直到收到最后一个数据。

我们来运行一下程序:

public class ProducerConsumerExample {
    public static void main(String[] args) {
        Drop drop = new Drop();
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}

程序输出如下:

MESSAGE RECEIVED: Mares eat oats
MESSAGE RECEIVED: Does eat oats
MESSAGE RECEIVED: Little lambs eat ivy
MESSAGE RECEIVED: A kid will eat ivy too

可以看到,我们以正确的顺序收到了所有的消息数据并成功的在Producer和Consumer之间完成了数据共享。

总结

本文讨论了Java的一些核心概念,更具体地说,我们聚焦在怎么使用wait()和notify()来解决同步问题,最后我们以一个简单例子说明了这些概念的使用。

值得一提的是这些都是低层次的API(wait、notify、notifyAll)。
有一些更高层次的API通常更简单且更好用,比如JDK中的Lock、Condition。关于这些可以看下我整理的一些文章

测试代码

参考

Guarded Blocks
Oracle官方并发教程之Guarded Blocks
Oracle Java Tutorials "Intrinsic Locks and Synchronization"
Oracle Java Tutorials "Questions and Exercises: Concurrency"

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