Java 线程通信之 wait/notify 机制
前言
Java 线程通信是将多个独立的线程个体进行关联处理,使得线程与线程之间能进行相互通信。比如线程 A 修改了对象的值,然后通知给线程 B,使线程 B 能够知道线程 A 修改的值,这就是线程通信。
<!-- more -->
wait/notify 机制
一个线程调用 Object 的 wait() 方法,使其线程被阻塞;另一线程调用 Object 的 notify()/notifyAll() 方法,wait() 阻塞的线程继续执行。
wai/notify 方法
方法 | 说明 |
---|---|
wait() | 当前线程被阻塞,线程进入 WAITING 状态 |
wait(long) | 设置线程阻塞时长,线程会进入 TIMED_WAITING 状态。如果设置时间内(毫秒)没有通知,则超时返回 |
wait(long, int) | 纳秒级别的线程阻塞时长设置 |
notify() | 通知同一个对象上已执行 wait() 方法且获得对象锁的等待线程 |
notifyAll() | 通知同一对象上所有等待的线程 |
实现 wait/notify 机制的条件:
- 调用 wait 线程和 notify 线程必须拥有相同对象锁。
- wait() 方法和 notify()/notifyAll() 方法必须在 Synchronized 方法或代码块中。
由于 wait/notify 方法是定义在java.lang.Object
中,所以在任何 Java 对象上都可以使用。
wait 方法
在执行 wait() 方法前,当前线程必须已获得对象锁。调用它时会阻塞当前线程,进入等待状态,在当前 wait() 处暂停线程。同时,wait() 方法执行后,会立即释放获得的对象锁。
下面通过案例来查看 wait() 释放锁。
首先查看不使用 wait() 方法时的代码执行情况:
package top.ytao.demo.thread.waitnotify; /** * Created by YangTao */ public class WaitTest { static Object object = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (object){ System.out.println("开始线程 A"); try { Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("结束线程 A"); } }, "线程 A").start(); new Thread(() -> { try { Thread.sleep(500L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (object){ System.out.println("开始线程 B"); System.out.println("结束线程 B"); } }, "线程 B").start(); } }
创建 A、B 两个线程,。首先在 B 线程创建后 sleep ,保证 B 线程的打印后于 A 线程执行。在 A 线程中,获取到对象锁后,sleep 一段时间,且时间大于 B 线程的 sleep 时间。
执行结果为:
从上图结果中,可以看到,B 线程一定等 A 线程执行完 synchronize 代码块释放对象锁后 A 线程再获取对象锁进入 synchronize 代码块中。在这过程中,Thread.sleep() 方法也不会释放锁。
当前在 A 线程 synchronize 代码块中执行 wait() 方法后,就会主动释放对象锁,A 线程代码如下:
new Thread(() -> { synchronized (object){ System.out.println("开始线程 A"); try { // 调用 object 对象的 wait 方法 object.wait(); Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("结束线程 A"); } }, "线程 A").start();
执行结果:
同时 A 线程一直处于阻塞状态,不会打印结束线程 A
。
wait(long) 方法是设置超时时间,当等待时间大于设置的超时时间后,会继续往 wait(long) 方法后的代码执行。
new Thread(() -> { synchronized (object){ System.out.println("开始线程 A"); try { object.wait(1000); Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("结束线程 A"); } }, "线程 A").start();
执行结果
同理,wait(long, int) 方法与 wait(long) 同样,只是多个纳秒级别的时间设置。
notify 方法
同样,在执行 notify() 方法前,当前线程也必须已获得线程锁。调用 notify() 方法后,会通知一个执行了 wait() 方法的阻塞等待线程,使该等待线程重新获取到对象锁,然后继续执行 wait() 后面的代码。但是,与 wait() 方法不同,执行 notify() 后,不会立即释放对象锁,而需要执行完 synchronize 的代码块或方法才会释放锁,所以接收通知的线程也不会立即获得锁,也需要等待执行 notify() 方法的线程释放锁后再获取锁。
notify()
下面是 notify() 方法的使用,实现一个完整的 wait/notify 的例子,同时验证发出通知后,执行 notify() 方法的线程是否立即释放锁,执行 wait() 方法的线程是否立即获取锁。
package top.ytao.demo.thread.waitnotify; /** * Created by YangTao */ public class WaitNotifyTest { static Object object = new Object(); public static void main(String[] args) { System.out.println(); new Thread(() -> { synchronized (object){ System.out.println("开始线程 A"); try { object.wait(); System.out.println("A 线程重新获取到锁,继续进行"); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("结束线程 A"); } }, "线程 A").start(); new Thread(() -> { try { Thread.sleep(500L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (object){ System.out.println("开始线程 B"); object.notify(); System.out.println("线程 B 通知完线程 A"); try { // 试验执行完 notify() 方法后,A 线程是否能立即获取到锁 Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("结束线程 B"); } }, "线程 B").start(); } }
以上 A 线程执行 wait() 方法,B 线程执行 notify() 方法,执行结果为:
执行结果中可以看到,B 线程执行 notify() 方法后,即使 sleep 了,A 线程也没有获取到锁,可知,notify() 方法并没有释放锁。
notify() 是通知到等待中的线程,但是调用一次 notify() 方法,只能通知到一个执行 wait() 方法的等待线程。如果有多个等待状态的线程,则需多次调用 notify() 方法,通知到线程顺序则根据执行 wait() 方法的先后顺序进行通知。
下面创建有两个执行 wait() 方法的线程的代码:
package top.ytao.demo.thread.waitnotify; /** * Created by YangTao */ public class MultiWaitNotifyTest { static Object object = new Object(); public static void main(String[] args) { System.out.println(); new Thread(() -> { synchronized (object){ System.out.println("开始线程 A"); try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("结束线程 A"); } }, "线程 A").start(); new Thread(() -> { try { Thread.sleep(500L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (object){ System.out.println("开始线程 B"); try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("结束线程 B"); } }, "线程 B").start(); new Thread(() -> { try { Thread.sleep(3000L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (object){ System.out.println("开始通知线程 C"); object.notify(); object.notify(); System.out.println("结束通知线程 C"); } }, "线程 C").start(); } }
先 A 线程执行 wait() 方法,然后 B 线程执行 wait() 方法,最后 C 线程调用两次 notify() 方法,执行结果:
notifyAll()
通知多个等待状态的线程,通过多次调用 notify() 方法实现的方案,在实际应用过程中,实现过程不太友好,如果是想通知所有等待状态的线程,可使用 notifyAll() 方法,就能唤醒所有线程。
实现方式,只需将上面 C 线程的多次调用 notify() 方法部分改为调用一次 notifyAll() 方法即可。
new Thread(() -> { try { Thread.sleep(3000L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (object){ System.out.println("开始通知线程 C"); object.notifyAll(); System.out.println("结束通知线程 C"); } }, "线程 C").start();
执行结果:
根据不同 JVM 的实现,notifyAll() 的唤醒顺序会有所不同,当前测试环境中,以倒序顺序唤醒线程。
实现生产者消费者模式
生产消费者模式就是一个线程生产数据进行存储,另一线程进行数据提取消费。下面就以两个线程来模拟,生产者生成一个 UUID 存放到 List 对象中,消费者读取 List 对象中的数据,读取完成后进行清除。
实现代码如下:
package top.ytao.demo.thread.waitnotify; import java.util.ArrayList; import java.util.List; import java.util.UUID; /** * Created by YangTao */ public class WaitNotifyModelTest { // 存储生产者产生的数据 static List<String> list = new ArrayList<>(); public static void main(String[] args) { new Thread(() -> { while (true){ synchronized (list){ // 判断 list 中是否有数据,如果有数据的话,就进入等待状态,等数据消费完 if (list.size() != 0){ try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // list 中没有数据时,产生数据添加到 list 中 list.add(UUID.randomUUID().toString()); list.notify(); System.out.println(Thread.currentThread().getName() + list); } } }, "生产者线程 A ").start(); new Thread(() -> { while (true){ synchronized (list){ // 如果 list 中没有数据,则进入等待状态,等收到有数据通知后再继续运行 if (list.size() == 0){ try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 有数据时,读取数据 System.out.println(Thread.currentThread().getName() + list); list.notify(); // 读取完毕,将当前这条 UUID 数据进行清除 list.clear(); } } }, "消费者线程 B ").start(); } }
运行结果:
生产者线程运行时,如果已存在未消费的数据,则当前线程进入等待状态,收到通知后,表明数据已消费完,再继续向 list 中添加数据。
消费者线程运行时,如果不存在未消费的数据,则当前线程进入等待状态,收到通知后,表明 List 中已有新数据被添加,继续执行代码消费数据并清除。
不管是生产者还是消费者,基于对象锁,一次只能一个线程能获取到,如果生产者获取到锁就校验是否需要生成数据,如果消费者获取到锁就校验是否有数据可消费。
一个简单的生产者消费者模式就以完成。
总结
等待/通知机制是实现 Java 线程间通信的一种方式,将多线程中,各个独立运行的线程通过相互通信来更高效的协作完成工作,更大效率利用 CPU 处理程序。这也是学习或研究 Java 线程的必学知识点。
推荐阅读
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
开源让软件更加安全了吗?
近日,软件和芯片设计公司 Synopsys 发布《2020年开源安全和风险分析报告》,指出不安全的开源软件已无处不在。一方面,99%的审计代码库中至少包含一个开源组件,另一方面,经过审核的代码库中有75%包含具有已知安全漏洞的开源组件,老化和废弃的开源组件也无处不在。 3月,安全和许可证合规性管理解决方案提供商 WhiteSource 同样发布了一份《2019年开源组件安全漏洞现状报告》。统计显示,2019年公开的开源软件漏洞数量激增至6000多个,增幅达近50%,原因包含开源软件应用的扩大。 两份报告指向同个现象——开源软件的应用已非常广泛,开源和“我们必须只使用专有代码”的想法间的战争已经结束了,取而代之的是对开源软件是否安全的讨论。 关注和应用的增加带来更多安全问题 WhiteSource 在报告中说明,开源软件漏洞数量的上升可以归因于开放源组件的广泛采用,过去几年开源社区的大量增长,以及媒体对最近一些数据泄露事件的报道,(使得人们)对开放源代码安全的关注提高。 正如 Synopsys 公司的报告中所提到的,开源组件和库是每个行业每个应用程序的基础。 Synopsys 公司的开源...
- 下一篇
监控系统设计
每日优鲜监控系统早期情况 系统覆盖不全 每日优鲜早期只有交易平台存在一套内部的业务监控系统,没有推广到全公司级别。大数据团队与自己的业务监控,运维团队有自己的基础监控。除了交易系统其他业务线的业务监控几乎为零,很多时候都是用户告知我们出问题了,而不是我们主动发现出问题了,导致问题发现的时候已经过去很久了。 监控类型不完善 监控内容主要是涉及日志中出现的数据统计,所以对PV、UV、JVM相关监控都没有,尤其对自身业务的监控几乎为零,我们无法实时的知道当前接口的访问量,错误率等信息;除此之外我们依赖的zookeeper、mq、redis、数据库等中间件的监控也基本没有,所以很难做到深入的排查。不过好在有一套pinpoint可以帮助故障和性能定位。但是这并不能代替业务监控。 监控系统选型和实现 选型 要实现一套监控系统,必须要保证数据的收集、存储和可视化,然后在基于此实现一套告警系统,最终实现数据的可视化与问题的触达。 可视化选型 在做监控系统选型的时候,最优先定下来的是可视化,即Grafana这套开源产品,因为其支持多数据源,同时也支持告警规则,除此之外其提供了一套完备的API,我们通过程...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS8编译安装MySQL8.0.19
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS7安装Docker,走上虚拟化容器引擎之路