从Guarded Block来看Java中的wait和notify方法
预备知识
概览
本文探究一下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 (<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.
*/
当一个线程调用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"
从Guarded Block来看Java中的wait和notify方法的更多相关文章
- Java中的equals和hashCode方法
本文转载自:Java中的equals和hashCode方法详解 Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要 ...
- Java中的equals和hashCode方法详解
Java中的equals和hashCode方法详解 转自 https://www.cnblogs.com/crazylqy/category/655181.html 参考:http://blog.c ...
- 将java中数组转换为ArrayList的方法实例(包括ArrayList转数组)
方法一:使用Arrays.asList()方法 1 2 String[] asset = {"equity", "stocks", "gold&q ...
- 转:Java中的equals和hashCode方法详解
转自:Java中的equals和hashCode方法详解 Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要重写这 ...
- 在java中为啥要重写toString 方法?
在java中为啥要重写toString 方法?下面以一个简单的例子来说明. 先定义一个test5类.并写它的get,set方法. package test5; public class Test5 { ...
- Java 中extends与implements使用方法
Java 中extends与implements使用方法 标签: javaclassinterfacestring语言c 2011-04-14 14:57 33314人阅读 评论(7) 收藏 举报 分 ...
- Java中各种(类、方法、属性)访问修饰符与修饰符的说明
类: 访问修饰符 修饰符 class 类名称 extends 父类名称 implement 接口名称 (访问修饰符与修饰符的位置可以互换) 访问修饰符 名称 说明 备注 public 可以被本项目的所 ...
- Java中替换HTML标签的方法代码
这篇文章主要介绍了Java中替换HTML标签的方法代码,需要的朋友可以参考下 replaceAll("\\&[a-zA-Z]{0,9};", "").r ...
- java中需要关注的3大方面内容/Java中创建对象的几种方法:
1)垃圾回收 2)内存管理 3)性能优化 Java中创建对象的几种方法: 1)使用new关键字,创建相应的对象 2)通过Class下面的new Instance创建相应的对象 3)使用I/O流读取相应 ...
随机推荐
- javaScript 必会基础知识
1.JavaScript是一种浏览器解析的轻量级脚本语言. 2.html.jsp等内部js代码写在<script></script>之间:外部js文件中书写js代码不能有< ...
- DiskLruCache缓存bitmap
public class MainActivity extends AppCompatActivity { private DiskLruCache diskLruCache; ImageView i ...
- Chrome FeHelper 插件下载地址
方便大家下载 下载可用,本人亲自测试 下载地址: 地址链接
- idea2019注册码,亲测可用(暂时不可用)!
原文链接:https://www.jianshu.com/p/702deab2447c 注册码: MNQ043JMTU-eyJsaWNlbnNlSWQiOiJNTlEwNDNKTVRVIiwibGlj ...
- mysql-connector-java各种版本下载地址
mysql-connector-java下载地址: http://mvnrepository.com/artifact/mysql/mysql-connector-java 目录 1.进去后选择自己的 ...
- uniapp swiper高度自适应问题
这里的话是想做一个比较常见的左右滑动更改tab的效果,引用了uview-ui中的u-tabs-swiper组件,需要结合swiper组件来使用 先来讲下主体思路:每个tab页(swiper-item) ...
- 16 String类
java中的所有的字符串文字(例如"abc","123")都可以看做是实现了此类的实例对象 eg: String str = new String(); str ...
- Luogu P2447 [SDOI2010]外星千足虫
题意 给定 \(n\) 个变量和 \(m\) 个异或方程,求最少需要多少个才能确定每个变量的解. \(\texttt{Data Range:}1\leq n\leq 10^3,1\leq m\leq ...
- openshift 平台上部署 gitlab代码仓库服务
背景: 本文档将以在openshift 平台上部署 gitlab 服务来验证集群各个服务组件的可用性以及熟悉openshift的使用方法.服务部署方式可以多种多样,灵活部署.本篇以常见的镜像部署方式来 ...
- PHP 获取当前页面的URL信息
//获取当前的域名: echo $_SERVER['SERVER_NAME']; //获取来源网址,即点击来到本页的上页网址 echo $_SERVER["HTTP_REFERER" ...