4、等待通知机制

等待通知机制

1、volatile,最轻量的通信/同步机制

	volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某
个变量的值,这新值对其他线程来说是立即可见的。

在这里插入图片描述

不加 volatile 时,子线程无法感知主线程修改了 ready 的值,从而不会退出循环,
而加了 volatile 后,子线程可以感知主线程修改了 ready 的值,迅速退出循环。
但是 volatile 不能保证数据在多个线程下同时写时的线程安全,volatile 最适用的
场景:一个线程写,多个线程读

2、等待/通知机制

	线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,
而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,
而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了
“做什么”(what)和“怎么做”(How),简单的办法是让消费者线程不断地循环
检查变量是否符合预期在 while 循环中设置不满足的条件,如果条件满足则退
出 while 循环,从而完成消费者的工作。却存在如下问题:
	1)、难以确保及时性。
	2)、难以降低开销。如果降低睡眠的时间,比如休眠 1 毫秒,这样消费者能
		更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无
		端的浪费。


	等待/通知机制则可以很好的避免,这种机制是指一个线程 A 调用了对象 O
的 wait()方法进入等待状态,而另一个线程 B 调用了对象 O 的 notify()或者
notifyAll()方法,线程 A 收到通知后从对象 O 的 wait()方法返回,进而执行
后续操作。上述两个线程通过对象 O 来完成交互,而对象上的 wait()和 notify/notifyAll()
的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

2.1、notify():

	通知一个在对象上等待的线程,使其从 wait 方法返回,而返回的前提是该线程
获取到了对象的锁,没有获得锁的线程重新进入 WAITING 状态。

2.2、notifyAll():

	通知所有等待在该对象上的线程

2.3、wait()

	调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断
才会返回.需要注意,调用 wait()方法后,会释放对象的锁

2.4、wait(long)

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

2.5、wait (long,int)

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

3、等待和通知的标准范式

3.1、等待方遵循如下原则:

1)获取对象的锁。
2)如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
	synchronized(对象){
		while(条件不满足){
			对象.wait();
		}
		// 对应的处理逻辑
	}

3.2、通知方遵循如下原则:

1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
	synchronized(对象){
		改变条件;
		对象.notifyAll();
	}
	在调用 wait()、notify()系列方法之前,线程必须要获得该对象的对象级
别锁,即只能在同步方法或同步块中调用 wait()方法、notify()系列方法,进
入 wait()方法后,当前线程释放锁,在从 wait()返回前,线程与其他线程竞
争重新获得锁,执行 notify()系列方法的线程退出调用了 notifyAll 的 synchronized
代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会
继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的
线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

3.3、notify 和 notifyAll 应该用谁

	尽可能用 notifyall(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我
们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程

4、等待超时模式实现一个连接池

	调用场景:调用一个方法时等待一段时间(一般来说是给定一个时间段),
如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超
时返回默认结果。

	假设等待时间段是 T,那么可以推断出在当前时间 now+T 之后就会超时
等待持续时间:REMAINING=T。
	超时时间:FUTURE=now+T
	// 对当前对象加锁
	public synchronized Object get(long mills) throws InterruptedException {
		long future = System.currentTimeMillis() + mills;
		long remaining = mills;
		// 当超时大于 0 并且 result 返回值不满足要求
		while ((result == null) && remaining > 0) {
			wait(remaining);
			remaining = future - System.currentTimeMillis();
		}
		return result;
	}
	客户端获取连接的过程被设定为等待超时的模式,也就是在 1000 毫秒内如
果无法获取到可用连接,将会返回给客户端一个 null。设定连接池的大小为 10
个,然后通过调节客户端的线程数来模拟无法获取连接的场景。

	它通过构造函数初始化连接的最大上限,通过一个双向队列来维护连接,调
用方需要先调用 fetchConnection(long)方法来指定在多少毫秒内超时获取连接,
当连接使用完成后,需要调用 releaseConnection(Connection)方法将连接放回
线程池

5、试题

5.1、调用 yield() 、sleep()、wait()、notify()等方法对锁有何影响?

	yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。
	
	调用 wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新
去竞争锁,锁竞争到后才会执行 wait 方法后面的代码。

	调用 notify()系列方法后,对锁无影响,线程只有在 syn 同步代码执行完后才
会自然而然的释放锁,所以 notify()系列方法一般都是 syn 同步代码的最后一行。

5.2、为什么 wait 和 notify 方法要在同步块中调用?

	主要是因为 Java API 强制要求这样做,如果你不这么做,你的代码会抛出
IllegalMonitorStateException 异常。其实真实原因是:
	这个问题并不是说只在 Java 语言中会出现,而是会在所有的多线程环境下出现。

	假如我们有两个线程,一个消费者线程,一个生产者线程。生产者线程的任务
可以简化成将 count 加一,而后唤醒消费者;消费者则是将 count 减一,而后
在减到 0 的时候陷入睡眠:
	// 生产者伪代码:
	count+1;
	notify();

	// 消费者伪代码:
	while(count<=0){
		wait();
		count--;
	}

这里面有什么问题呢?

生产者是两个步骤:
	1. count+1;
	2. notify();
	
消费者也是两个步骤:
	1. 检查 count 值;
	2. 睡眠或者减一;


	万一这些步骤混杂在一起呢?比如说,初始的时候 count 等于 0,这个时候
消费者检查 count 的值,发现 count 小于等于 0 的条件成立;就在这个时候,
发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了,
也就是发出了通知,准备唤醒一个线程。这个时候消费者刚决定睡觉,还没睡呢,
所以这个通知就会被丢掉。紧接着,消费者就睡过去了……

在这里插入图片描述

这就是所谓的 lost wake up 问题。


那么怎么解决这个问题呢?
	现在我们应该就能够看到,问题的根源在于,消费者在检查 count 到调用
wait()之间,count 就可能被改掉了。

这就是一种很常见的竞态条件。

很自然的想法是,让消费者和生产者竞争一把锁,竞争到了的,才能够修改count 的值。

5.3、为什么你应该在循环中检查等待条件?

	处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等
待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒
来时,不能认为它原来的等待状态仍然是有效的,在 notify()方法调用之后和等
待线程醒来之前这段时间它可能会改变。这就是在循环中使用 wait()方法效果更
好的原因。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值