前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java多线程学习(四)等待/通知(wait/notify)机制

Java多线程学习(四)等待/通知(wait/notify)机制

原创
作者头像
用户2164320
发布于 2018-06-22 11:53:42
发布于 2018-06-22 11:53:42
2.1K01
代码可运行
举报
运行总次数:1
代码可运行

我自己总结的Java学习的系统知识点以及面试问题,目前已经开源,会一直完善下去,欢迎建议和指导欢迎Star: https://github.com/Snailclimb/Java-Guide

本节思维导图:

本节思维导图
本节思维导图

一 等待/通知机制介绍

1.1 不使用等待/通知机制

当两个线程之间存在生产和消费者关系,也就是说第一个线程(生产者)做相应的操作然后第二个线程(消费者)感知到了变化又进行相应的操作。比如像下面的whie语句一样,假设这个value值就是第一个线程操作的结果,doSomething()是第二个线程要做的事,当满足条件value=desire后才执行doSomething()。

但是这里有个问题就是:第二个语句不停过通过轮询机制来检测判断条件是否成立。如果轮询时间的间隔太小会浪费CPU资源,轮询时间的间隔太大,就可能取不到自己想要的数据。所以这里就需要我们今天讲到的等待/通知(wait/notify)机制来解决这两个矛盾。

代码语言:txt
复制
    while(value=desire){
        doSomething();
    }

1.2 什么是等待/通知机制?

通俗来讲:

等待/通知机制在我们生活中比比皆是,一个形象的例子就是厨师和服务员之间就存在等待/通知机制。

  1. 厨师做完一道菜的时间是不确定的,所以菜到服务员手中的时间是不确定的;
  2. 服务员就需要去“等待(wait)”;
  3. 厨师把菜做完之后,按一下铃,这里的按铃就是“通知(nofity)”;
  4. 服务员听到铃声之后就知道菜做好了,他可以去端菜了。

用专业术语讲:

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()/notifyAll()方法,线程A收到通知后退出等待队列,进入可运行状态,进而执行后续操作。上诉两个线程通过对象O来完成交互,而对象上的wait()方法notify()/notifyAll()方法的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

1.3 等待/通知机制的相关方法

方法名称

描述

notify()

随机唤醒等待队列中等待同一共享资源的 “一个线程”,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知“一个线程”

notifyAll()

使所有正在等待队列中等待同一共享资源的 “全部线程” 退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现

wait()

使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒

wait(long)

超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回

wait(long,int)

对于超时时间更细力度的控制,可以达到纳秒

二 等待/通知机制的实现

2.1 我的第一个等待/通知机制程序

MyList.java

代码语言:txt
复制
public class MyList {
	private static List<String> list = new ArrayList<String>();

	public static void add() {
		list.add("anyString");
	}

	public static int size() {
		return list.size();
	}

}

ThreadA.java

代码语言:txt
复制
public class ThreadA extends Thread {

	private Object lock;

	public ThreadA(Object lock) {
		super();
		this.lock = lock;
	}

	@Override
	public void run() {
		try {
			synchronized (lock) {
				if (MyList.size() != 5) {
					System.out.println("wait begin "
							+ System.currentTimeMillis());
					lock.wait();
					System.out.println("wait end  "
							+ System.currentTimeMillis());
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}

ThreadB.java

代码语言:txt
复制
public class ThreadB extends Thread {
	private Object lock;

	public ThreadB(Object lock) {
		super();
		this.lock = lock;
	}

	@Override
	public void run() {
		try {
			synchronized (lock) {
				for (int i = 0; i < 10; i++) {
					MyList.add();
					if (MyList.size() == 5) {
						lock.notify();
						System.out.println("已发出通知!");
					}
					System.out.println("添加了" + (i + 1) + "个元素!");
					Thread.sleep(1000);
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}

Run.java

代码语言:txt
复制
public class Run {

	public static void main(String[] args) {

		try {
			Object lock = new Object();

			ThreadA a = new ThreadA(lock);
			a.start();

			Thread.sleep(50);

			ThreadB b = new ThreadB(lock);
			b.start();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}

}

运行结果:

运行结果
运行结果

从运行结果:"wait end 1521967322359"最后输出可以看出,<font color="red">notify()执行后并不会立即释放锁。</font>下面我们会补充介绍这个知识点。

synchronized关键字可以将任何一个Object对象作为同步对象来看待,而Java为每个Object都实现了等待/通知(wait/notify)机制的相关方法,它们必须用在synchronized关键字同步的Object的临界区内。通过调用wait()方法可以使处于临界区内的线程进入等待状态,同时释放被同步对象的锁。而notify()方法可以唤醒一个因调用wait操作而处于阻塞状态中的线程,使其进入就绪状态。被重新唤醒的线程会视图重新获得临界区的控制权也就是锁,并继续执行wait方法之后的代码。如果发出notify操作时没有处于阻塞状态中的线程,那么该命令会被忽略。

如果我们这里不通过等待/通知(wait/notify)机制实现,而是使用如下的while循环实现的话,我们上面也讲过会有很大的弊端。

代码语言:txt
复制
 while(MyList.size() == 5){
        doSomething();
    }

2.2线程的基本状态

上面几章的学习中我们已经掌握了与线程有关的大部分API,这些API可以改变线程对象的状态。如下图所示:

线程的基本状态切换图
线程的基本状态切换图
  1. 新建(new):新创建了一个线程对象。
  2. 可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取cpu的使用权。
  3. 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。
  4. 阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有 机会再次获得cpu timeslice转到运行(running)状态。阻塞的情况分三种:

(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放 入等待队列(waitting queue)中。

代码语言:txt
复制
(二). **同步阻塞**:运行(running)的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
代码语言:txt
复制
(三). **其他阻塞**: 运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
  1. 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

备注:

可以用早起坐地铁来比喻这个过程:

还没起床:sleeping

起床收拾好了,随时可以坐地铁出发:Runnable

等地铁来:Waiting

地铁来了,但要排队上地铁:I/O阻塞

上了地铁,发现暂时没座位:synchronized阻塞

地铁上找到座位:Running

到达目的地:Dead

2.3 notify()锁不释放

<font color="red">当方法wait()被执行后,锁自动被释放,但执行完notify()方法后,锁不会自动释放。必须执行完notify()方法所在的synchronized代码块后才释放。</font>

下面我们通过代码验证一下:

(完整代码:https://github.com/Snailclimb/threadDemo/tree/master/src/wait_notifyHoldLock

<font size="2">带wait方法的synchronized代码块</font>

代码语言:txt
复制
			synchronized (lock) {
				System.out.println("begin wait() ThreadName="
						+ Thread.currentThread().getName());
				lock.wait();
				System.out.println("  end wait() ThreadName="
						+ Thread.currentThread().getName());
			}

<font size="2">带notify方法的synchronized代码块</font>

代码语言:txt
复制
			synchronized (lock) {
				System.out.println("begin notify() ThreadName="
						+ Thread.currentThread().getName() + " time="
						+ System.currentTimeMillis());
				lock.notify();
				Thread.sleep(5000);
				System.out.println("  end notify() ThreadName="
						+ Thread.currentThread().getName() + " time="
						+ System.currentTimeMillis());
			}

如果有三个同一个对象实例的线程a,b,c,a线程执行带wait方法的synchronized代码块然后bb线程执行带notify方法的synchronized代码块紧接着c执行带notify方法的synchronized代码块。

<font size="2">运行效果如下:</font>

运行效果
运行效果

<font color="red">这也验证了我们刚开始的结论:必须执行完notify()方法所在的synchronized代码块后才释放。</font>

2.4 当interrupt方法遇到wait方法

<font color="red">当线程呈wait状态时,对线程对象调用interrupt方法会出现InterrupedException异常。</font>

<font size="2">Service.java</font>

代码语言:txt
复制
public class Service {
	public void testMethod(Object lock) {
		try {
			synchronized (lock) {
				System.out.println("begin wait()");
				lock.wait();
				System.out.println("  end wait()");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
			System.out.println("出现异常了,因为呈wait状态的线程被interrupt了!");
		}
	}
}

<font size="2">ThreadA.java</font>

代码语言:txt
复制
public class ThreadA extends Thread {

	private Object lock;

	public ThreadA(Object lock) {
		super();
		this.lock = lock;
	}

	@Override
	public void run() {
		Service service = new Service();
		service.testMethod(lock);
	}

}

<font size="2">Test.java</font>

代码语言:txt
复制
public class Test {

	public static void main(String[] args) {

		try {
			Object lock = new Object();

			ThreadA a = new ThreadA(lock);
			a.start();

			Thread.sleep(5000);

			a.interrupt();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}

}

<font size="2">运行结果:</font>

运行结果
运行结果

参考:

《Java多线程编程核心技术》

《Java并发编程的艺术》

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
技术分享 | 我的内存去哪儿?生产实践
爱可生 DBA 团队成员,负责项目日常问题处理及公司平台问题排查。热爱 IT,喜欢在互联网里畅游,擅长摄影、厨艺,不会厨艺的 DBA 不是好司机,didi~
爱可生开源社区
2020/09/29
6590
技术分享 | 我的内存去哪儿?生产实践
Percona Server 5.1 内存过量消耗分析3
我注意到最后两项(内存消耗最大两项),分别是 21.19G (22221844K/1024/1024) 和 7.44G (7801584K/1024/1024)怎么会有这么多的内存消耗呢~~非常奇怪!
franket
2022/07/09
5410
数据库运行一段时间mysqld占用内存越来越高达到90%
操作系统:CentOS Linux release 7.5.1804 (Core) 64核64G
wangwei-dba
2021/07/27
19.6K0
Percona Server 5.1 内存过量消耗分析4
由于 innodb_buffer_pool_size 和 query_cache_size 都是我手动配置的,所以这个差异报告让我立刻注意到了 innodb_ibuf_max_size
franket
2022/07/09
5160
MySQL导致的CPU高负载问题
在某个新服务器上,新建了一个MySQL的实例,该服务器上面只有MySQL这一个进程,但是CPU的负载却居高不下,使用top命令查询的结果如下:
AsiaYe
2019/11/06
2.4K0
Percona Server 5.1 内存过量消耗分析5
发现都正好是 innodb_buffer_pool_size 的一半,于是开始查文档弄清楚 innodb_ibuf_max_size 参数的意义
franket
2022/07/09
5570
《叶问》33期,MGR最佳配置参考,PFS里的监测指标要全开吗,mysqld进程占用内存过高怎么排查
在「3306π」社区广州站5月22日的分享会上,万里数据库CTO娄帅给出了他建议的配置参考,我们一起来看下:
老叶茶馆
2021/05/31
1.2K0
MYSQL 8 内存使用分析到底我的内存都跑哪了
人生可悲的事情是,你不知道问题如何解决,并且困惑中, 而更可悲的是,你根本就不知道自己不知道, 当然从另一个角度,那也是一种"幸福".
AustinDatabases
2021/03/16
4.5K0
MYSQL  8 内存使用分析到底我的内存都跑哪了
Percona Server 5.1 内存过量消耗分析6
由于它并不能动态进行调整,所以必须安排一次数据库的启停,在配置文件中对 innodb_ibuf_max_size 进行限定就可以有效解决此问题
franket
2022/07/09
6310
故障分析 | MySQL 耗尽主机内存一例分析
开发人员反馈,有一台服务器内存几乎被 MySQL 耗尽了,执行 top 命令,输出如下:
爱可生开源社区
2022/07/05
1.3K0
MySQL内存到底消耗在哪里?
一说起MySQL使用的内存,你可能会想到各种buffer,最著名的莫过于innodb buffer pool了,它是内存使用的大户,还有sort buffer等等。除了这些buffer之外,可能还有一些细枝末节,今天我们来总结一下。
AsiaYe
2021/12/04
3.2K0
Percona Server 5.1 内存过量消耗分析1
分析问题初步推断有两种情况:参数配置不当内存泄漏关于参数配置不当,我分析完各种buffer,cache参数配置后没有发现异常或特别严重的错误,于是尝试从内存泄漏的角度来寻找突破口----分析工具pmap : 用来生成一个进程的内存使用报表The pmap command reports the memory map of a process or processes.pt-config-diff : 用来比较Mysql 配置文件的差异pt-config-diff diffs MySQL configurat
franket
2022/07/09
6440
MySQL OOM(内存溢出)的排查思路及优化方法
大部分情况下,会杀掉导致OOM的进程,然后系统恢复。通常我们会添加对内存的监控报警,例如:当memory或swap使用超过90%时,触发报警通知,需要及时介入排查。
MySQL轻松学
2019/08/01
9.9K0
linux内存使用情况分析(free + top)
文章转载自:https://www.cnblogs.com/pengdonglin137/p/3315124.html
我是李超人
2020/08/21
2.9K0
Changes in GreatSQL 8.0.25-16(2022-5-16)
该节点仅参与MGR投票仲裁,不存放实际数据,也无需执行DML操作,因此可以用一般配置级别的服务器,在保证MGR可靠性的同时还能降低服务器成本。
GreatSQL社区
2022/05/16
3850
Percona Server 的安装及tokudb引擎的安装笔记
yum localinstall Percona-Server-client-57-5.7.18-15.1.el6.x86_64.rpm  Percona-Server-shared-57-5.7.18-15.1.el6.x86_64.rpm Percona-Server-server-57-5.7.18-15.1.el6.x86_64.rpm  Percona-Server-tokudb-57-5.7.18-15.1.el6.x86_64.rpm
保持热爱奔赴山海
2019/09/18
8530
MySQL实战第三十三讲- 我查这么多数据,会不会把数据库内存打爆?
我经常会被问到这样一个问题:我的主机内存只有 100G,现在要对一个 200G 的大表做全表扫描,会不会把数据库主机的内存用光了?
越陌度阡
2022/05/06
6020
MySQL实战第三十三讲- 我查这么多数据,会不会把数据库内存打爆?
Changes in GreatSQL 8.0.25-16(2022-5-16)
该节点仅参与MGR投票仲裁,不存放实际数据,也无需执行DML操作,因此可以用一般配置级别的服务器,在保证MGR可靠性的同时还能降低服务器成本。
GreatSQL社区
2023/02/24
4200
由hugepage设置导致的数据库事故(r4笔记第28天)
近期客户需要希望提高业务处理能力,在现有的系统中加入几台weblogic服务器,所以需要增加以下连接数的配置,但是同时他们想对现有系统的设置一些变更,发送了一个清单给我们。 大体的变更如下: Change Processes from 10000 to 18000 Change PGA from 10G to 20G Change Buffer Cache from 20G to 40G Change Shared pool from 10G to 20G HugePage from 60 GB t
jeanron100
2018/03/15
7540
Mysql的qps高DB随时可能挂掉时的处理方法
使用Mysql中如果CPU在95%及以上,Qps突然增到2万以上,这时Mysql随时有死去风险。
杨漆
2021/08/08
2.2K0
Mysql的qps高DB随时可能挂掉时的处理方法
推荐阅读
相关推荐
技术分享 | 我的内存去哪儿?生产实践
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验