wait与notify
注意是锁对象调用这些方法,锁对象调用wait,他们都属于同一个对象的waitset
- Ower线程发现条件不满足,调用wait方法,即可进入WaitSet,变为Waiting状态(Ower线程就是拿到锁的线程,进入之后可以调用wait方法来让自己进入waiting状态。WaitSet是一个集合,里面专门存放waiting中的线程)
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU
- BLOCKED线程会在Ower线程释放锁时唤醒(也就是拿到锁的线程,调用了wait,就会释放锁,此时,就会唤醒那些阻塞的线程,他们就会来争取锁)
- WAITING线程线程会在Ower线程调用notify或notify all时唤醒,但唤醒后并不意味着立刻获得锁,仍需要进入队列,重新竞争。(也就是说,其他那些阻塞的线程拿到锁之后,可以调用notify或者notifyall,来唤醒前面调用了wait的线程)
梳理一下逻辑:也就是说A线程拿到锁,锁对象调用了wait(),就会使A线程进入WaitSet集合,变成Waiting状态,并且会释放锁。此时就会唤醒那些阻塞的线程来竞争锁,就假如是B线程拿到了锁,他就可以进行线程运行了,此时,他可以调用notify()或者是notifyAll()来唤醒那些进入waiting状态的线程,来重新竞争锁
API介绍
- obj.wait() 让进入 object 监视器(也就是Monitor,也就是拿到锁之后的执行区域)的线程到 waitSet 等待
- obj.wait(long timeout) 也就是有等待的时间,时间过了还没被notify,我也被唤醒继续向下执行
- obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
也就是说同一把锁对象调用wait,他们就属于同一个waitset集合里面的waiting线程。同一把锁对象调用notify,也就是唤醒同一把锁的waitset里面的线程!!!
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}).start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}).start();
// 主线程两秒后执行
sleep(2);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
// obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
这一把锁,就包含了所有使用这个锁的所有线程(好好理解这句话)
注意:wait()之后被唤醒,会继续向下执行代码,就类似于sleep()到时间了一样
wait和notify正确使用姿势
开始之前先看看sleep(long n) 和 wait(long n) 的区别 (重中之重)
- sleep 是 Thread 方法,而 wait 是 Object 的方法 (重中之重)
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要
和 synchronized 一起用 (重中之重) - sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁 (重中之重)
- 但是他们有个共同点,就是状态都是TIME_WAITING状态,有时间的等待
将来对象锁我们都把它设置成static final,表示不可变,确保锁的都是同一个对象
例子1
这样好不好?
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
//此时肯定不行咯。sleep都是不释放锁的
//但是假如是wait就可以加锁,因为wait会释放锁
hasCigarette = true;
log.debug("烟到了噢!");
}, "送烟的").start();
}
不好
- 其它干活的线程,都要一直阻塞,效率太低
- 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
- 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加synchronized 就好像 main 线程是翻窗户进来的
- 解决方法,使用 wait - notify 机制
改进
没烟就使用对象锁wait(),送烟的使用对象锁来notifyall()。也就是改成room.wait()和room.notify()。再次说白了,也就是锁对象来控制线程之间的协作!!!而不是线程本身的Thread的方法
例子2
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notify();
}
}, "送外卖的").start();
}
好不好与改进
外卖送到了,唤醒的确实等烟的小南,这样错误的唤醒了。
- notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
- 解决方法,改为 notifyAll(此时,两个线程都会被唤醒,小女有外卖,就可以干活了,但是小南没有烟,还是没干成活,继续等待。像这样,虽然解决了虚假唤醒的问题,但还是会有线程因为条件不满足,而继续执行不想要的逻辑。因为唤醒之后就会继续向下执行嘛,你想的是条件成立再唤醒,继续向下执行,而此时是条件不成立,也被唤醒继续向下执行了!!!)(重中之重)
进一步解决
问题:改为 notifyAll(此时,两个线程都会被唤醒,小女有外卖,就可以干活了,但是小南没有烟,还是没干成活,继续等待。像这样,虽然解决了虚假唤醒的问题,但还是会有线程因为条件不满足,而继续执行不想要的逻辑。因为唤醒之后就会继续向下执行嘛,你想的是条件成立再唤醒,继续向下执行,而此时是条件不成立,也被唤醒继续向下执行了!!!)
解决方法:把线程里面的wait()方法用while条件判断包裹,这样只有成立了,才会跳出wait()方法,如下:
while(条件){
log.debug("条件不成立,继续等待")
try{
objLock.wait();
}catch(Exception e){
e.printStackTrace()
}
}
这样他就会因为条件不成立,即便被唤醒,也会循环往复的wait()等待,直到被唤醒时,条件也成立了,才会跳出循环,继续向下执行,去执行条件成立后期望执行的代码。
这样就真正解决了虚假唤醒的问题,还是要理解,同一把锁的线程才在同一个waitset集合里面
总结wait与notify的正确使用姿势
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
同步模式之保护性暂停
定义
即Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
- 如果有结果不断从一个线程到另一个线程,那么可以使用消息队列(见生产者/消费者)
- JDK中,join的实现,Future的视线,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
实现
实现的关键在于中间的这个桥梁,“保护对象”
@Slf4j
public class JUCStudy {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(() -> {
//等待结果
log.debug("等待结果");
guardedObject.getResponse();
},"t1").start();
new Thread(() -> {
log.debug("执行下载");
try {
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//假装下载了三秒后有了结果
Object obj = new Object();
guardedObject.setResponse(obj);
log.debug("结果有了");
},"t2").start();
}
}
class GuardedObject {
//结果
private Object response;
//获取结果的方法
public Object getResponse() {
synchronized (this) {
//没有结果,就一直等待
while(response == null) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}
//产生结果的方法
public void setResponse(Object response) {
synchronized (this) {
//产生结果
this.response = response;
//有结果了唤醒那些等待的线程
this.notifyAll();
}
}
}
上面可以看出,就是过了大约3秒,也就是下载完,有了结果之后,t1线程获得结果了,才继续执行。说白了,意思就是吧两个要配合的线程绑定在同一个对象里面,例如共享的资源,等待的结果啥的。然后两个线程之间就用这个对象锁来加锁。互相wait和nitifyAll,假如只有一个线程等待就可以用notify即可。因为他是唤醒同一个锁对象里的线程
对保护性暂停扩展—增加超时的效果
上面那个例子,假如下载的时间不是3秒,当网络不好时一直都在下载,那另一个等待结果的线程就会一直等待。就不好。此时,我们就有一个场景,线程1等待线程2的结果,我只想等一会,等超时了我就不等了,要怎么实现呢?
方法改进
//获取结果的方法(给方法加个参数---超时参数,传入之后可以选择最多等待多久)
public Object getResponse(long timeout) {
synchronized (this) {
//没有结果,就一直等待
//开始时间
long begin = System.currentTimeMillis();
//经历时间
long passedTime = 0;
while(response == null) {
//这一轮循环应该等待的时间
long waitTime = timeout - passedTime;//优化,可利用waitTime替换简化下面的参数,以及passedTime >= timeout===>waitTime <= 0
//经历的时间,超过最大时间,就退出
if (passedTime >= timeout) {
break;
}
try {
/*
注意,这里也是还要加超时间的,因为本身就是需要这个wait()等待两秒先,再去通过开始时间经历时间来执行退出逻辑
但是只写timeout,应对不了虚假唤醒的情况,假如过了一秒被虚假唤醒,还是没有结果,还是得循环,那下一次再等两秒,一共就是3秒了
所以一开始等了1秒了,被唤醒之后下次就不需要等两秒,而是等待时间-已经等过的时间才对
所以已经写timeout-passedTime
*/
this.wait(timeout - passedTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
//求得经历时间,假如已经过了要等待的时间,就退出循环
passedTime = System.currentTimeMillis() - begin;
}
return response;
}
}
总结两点,第一点:不是单单的传入timeout时间参数进去wait()方法就可以了,因为他是一个while循环,目的是时间到了让他主动退出循环,所以需要的是一个开始等待时间和经过时间,来算出等了多久,跟timeout对比之后来判断时候否退出循环,继续执行后面的代码。第二点:为了应对虚假唤醒的情况,也不能单单只写一个timeout,被虚假唤醒,又没有结果,只能再次循环,因为下一次等并不需要等原来的时间,因为都已经等过了,所以应该是timeout-已经等过的时间才对
join原理
跟保护性暂停的不同,保护性暂停是一个线程等待另一个线程的结果
而join是一个线程等待另一个线程的结束
join带时间的源码,其实就跟上面写的代码很像
实际上join就是运用了保护性暂停模式
保护性暂停扩展—Future
图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员
如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理
重点例子 (生产消费一对一)
用 Id 来标识 Guarded Object
@Slf4j
public class JUCStudy {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new People().start();
}
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (Integer id : Mailboxes.getIds()) {
new Postman(id,"内容"+id).start();
}
}
}
/**
* 居民类
*/
@Slf4j(topic = "c.People")
class People extends Thread {
@Override
public void run() {
//收信
GuardedObject guardedObject = Mailboxes.createGuardedObject();
log.debug("开始收信-id:{}",guardedObject.getId());
Object mail = guardedObject.get(5000);
log.debug("收到信-id:{},内容:{}",guardedObject.getId(),mail);
}
}
/**
* 邮递员类
*/
@Slf4j(topic = "c.Postman")
class Postman extends Thread {
private int id;
private String mail;
public Postman(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
//送信
GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
log.debug("送信-id:{},内容:{}",id,mail);
guardedObject.complete(mail);
}
}
/**
* 中间解耦类
*/
class Mailboxes {
//因为这个对象是会被多个线程访问到的,所以得用hashtable,目前只学了这个。这里就是多个有Id的小格子,在一个邮件架子上
private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
private static int id = 1;
// 产生唯一 id
private static synchronized int generateId() {
return id++;
}
//根据ID获取 Object (获取后应该把邮件删除,注意:get是根据键返回值,而remove是根据键返回值,同时还把这个键值对删除掉,真牛,刚好符合业务逻辑,不用获取了保存、再删除、再返回)
public static GuardedObject getGuardedObject(int id) {
return boxes.remove(id);
}
//创建GuardedObject
public static GuardedObject createGuardedObject() {
GuardedObject go = new GuardedObject(generateId());
boxes.put(go.getId(), go);
return go;
}
//返回所有对象
public static Set<Integer> getIds() {
return boxes.keySet();
}
}
class GuardedObject {
// 标识 Guarded Object
private int id;
public GuardedObject(int id) {
this.id = id;
}
public int getId() {
return id;
}
// 结果
private Object response;
// 获取结果
// timeout 表示要等待多久 2000
public Object get(long timeout) {
synchronized (this) {
// 开始时间 15:00:00
long begin = System.currentTimeMillis();
// 经历的时间
long passedTime = 0;
while (response == null) {
// 这一轮循环应该等待的时间
long waitTime = timeout - passedTime;
// 经历的时间超过了最大等待时间时,退出循环
if (timeout - passedTime <= 0) {
break;
}
try {
this.wait(waitTime); // 虚假唤醒 15:00:01
} catch (InterruptedException e) {
e.printStackTrace();
}
// 求得经历时间
passedTime = System.currentTimeMillis() - begin; // 15:00:02 1s
}
return response;
}
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}
结果:
解释
首先,上面的Mailboxs和GuardedObject其实是可以复用的,可以起一个通用点的名字,在项目中可以直接拿来用。跟业务相关的是邮递员和居民
结果产生者和结果的消费者是一一对应的关系
也就是一个邮递员送一个信给居民,而不是一个邮递员承包所有信件
private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
我觉得主要是还是上面这一行代码,用线程安全的HashTable来装id和保护对象类
其实,中间那个Futures,也就是所说的解耦类,里面其实就是对保护对象的装、增、删、获取的操作。也就是个盒子,装了很多有对应id的保护对象。然后产生结果的,就根据所有id来产生所有结果。然后要等待结果的,就根据id来等待,直到产生结果的产生完。不知道这样能不能理解
邮递员获取一个个信件,根据id来送信。收信的根据id,来等待信件的送来。
异步模式之生产者/消费者
要点:
- 与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以采用平衡生产和消费的线程资源
- 生产者仅负责生产结果数据,不关数据后续该如何处理,而消费者专心处理数据结果,不关心数据从何而来
- 消息队列是有容量限制的,满时不会再加入数据,空时不会在消耗数据(说白了满了就不会再从生产者获取,空的时候不会在推送消息出去给消费者)
- JDK中各种阻塞队列,采用的就是这种模式
例子 重点
@Slf4j(topic = "c.study")
public class JUCStudy {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id=i;
new Thread(() -> {
queue.put(new Message(id,"值"+id));
},"生产者"+i).start();
}
new Thread(() -> {
while (true) {
//隔一秒消费一个消息
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
queue.take();
}
},"消费者").start();
}
}
@Slf4j(topic = "c.MessageQueue")
//消息队列类,Java线程之间的通信
class MessageQueue{
//消息队列集合,双向链表比较合适
private LinkedList<Message> list = new LinkedList<>();
//队列容量
private int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
}
//获取消息
public Message take() {
//先检查队列是否为空
synchronized (list){
while (list.isEmpty()){
try {
log.debug("队列空了,消费者线程进入等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//从队列的头部获取元素(消息)返回
Message message = list.removeFirst();
log.debug("已消费消息{}",message);
list.notifyAll();
return message;
}
}
//存入消息
public void put(Message message){
synchronized (list){
//检查队列是否满了
while (list.size() == capacity){
try {
log.debug("队列满了,生产者线程进入等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//从队列尾部插入元素
list.addLast(message);
log.debug("已生产消息{}",message);
list.notifyAll();
}
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
final class Message{
private int id;
private Object value;
}
运行结果:
如上图所示:生产了三条消息,也消费了三条消息,注意,进程还是一直在运行的,因为消费者一直在循环消费,消费完了,进入等待状态,等待生产者生产。代码的画,很容易理解,主要注意的点就是加锁,和对象锁的notifyAll与wait的正确搭配使用
Park和Unpark方法
基本使用
他们是LockSupport类中的方法
//暂停当前线程
LockSupport.park();//该线程进入WAITING状态
//恢复某个线程的运行
LockSupport.unpark(线程对象);
先park再unpark
@Slf4j(topic = "c.study")
public class JUCStudy {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
try {
sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
运行结果:
例子也很简单,主线程unpark了t1线程。值得注意的是park之后是WAITING状态
前面其实也有讲过,unpark之后,线程再执行park是无效的,因为unpark之后会把打断标记置为true,park对被打断过的线程是无效的。我记得前面应该是这样讲的
特点
与 Object 的 wait & notify 相比
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
- park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify