三种让线程等待和唤醒的方法
1)使用Object中的wait()方法让线程等待,使用`Object`中的`notify()`方法唤醒线程
2)使用`JUC`包中`Condition`的`await()`方法让线程等待,使用`signal()`方法唤醒线程
3)`LockSupport`类可以阻塞当前线程以及唤醒指定被阻塞的线程
(`park()`和`unpark()`)
传统的synchronized和Lock实现等待唤醒通知的约束
代码Demo
static Object objectLock=new Object();
new Thread(()->{
synchronized(objectLock){
objectLock.wait();
}
},"A").start();
new Thread(()->{
synchronized(objectLock){
objectLock.notify();
}
},"B").start();
发生的2类异常
1)wait方法和notify方法,两个都去掉同步代码块??
异常情况:会报IllegalMonitorStateException
异常
2)将notify放在wait方法前面
异常情况:程序无法执行,无法唤醒
等待中的线程才会被唤醒,否则无法唤醒
总结
i)wait和notify方法必须要在同步块或者方法里面,且成对出现使用
ii)先wait后notify
LockSupport
是用来创建锁和其他同步类的基本线程阻塞原语
通过park()
和unpark(thread)
方法来实现阻塞和唤醒线程
使用了一种名为Permit(许可)
来做到阻塞和唤醒线程
每个线程都有一个许可,许可permit只有2个值1和0,默认是0
可以把许可看作一种(0,1)信号量,但许可的累加上限是1
park源码
permit默认为0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回
unpark(thread)源码
调用unpark(thread)方法后,会将thread线程的许可permit设置为1(多次调用unpark方法,不会累加,permit还是1)会唤醒thread线程,之前阻塞的LockSupprt.park()方法会立即返回
总结
1)无锁块要求
2)没有先唤醒后等待的顺序要求(unpark可以在park之前执行)
面试题
1)为什么可以先唤醒线程(unpark)后阻塞线程(park)?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺地凭证消费,不会阻塞
2)为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证,而调用两次park却需要消费两个凭证,凭证不够,不能放行,则阻塞在那
AQS(AbstractQueuedSynchronizer)
是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态
这个队列就是AQS的抽象表现,它将请求共享资源的线程封装成队列的节点(Node),通过CAS,自旋以及LockSupprt.park()的方式,维护state遍历,使并发达到同步的控制效果
AQS内部体系架构
AQS同步队列的基本结构
AQS=state(同步状态)+CLH队列
包括了头、尾、前、后指针
AQS内有Node类:
Node=waitState+前后指针指向
包括int变量waitState(等待状态:等待区其他线程的等待状态)成员变量
AQS源码深度解读
ReentrantLock原理:使用Sync锁,Sync又实现了AQS
公平锁和非公平锁:
在创建完公平/非公平锁后,调用lock方法会进行加锁,最终都会调用到acquire方法
!hasQueuedPredecessors()
lock()
当加锁时(公平/不公平锁),先用CAS抢占,若抢占不了,进入acquire()方法
acquire()
tryAcquire()
不公平锁的抢占(nonfairTryAcquire)
i)c==0,表示之前抢占的线程A已经用完了,现在线程B可以抢占
ii)当前线程current与要进行抢占的线程如果是同一个线程,则使用重入锁,并且state+1
iii)前面2块都不满足,则抢占不超过,返回false
addWaiter
此时node指线程B,第一个线程进入等待队列时,第一个节点并不是该线程,而是系统自己新建的一个虚节点(哨兵节点)prev
这里的for(;;)表示自旋锁
acquireQueued()
i)node.predecessor()表示得到前一个节点
第一块表示如果是哨兵节点,并且抢占线程A的位置成功,就成功返回
ii)第二块中的shouldParkAfterFailedAcquire()
如果不是哨兵节点, 则将waitStatus置为当前的
表示将哨兵节点的值变为-1,由于自旋锁,再次循环,这次p和node的waitStatus一致。进入下一步,parkAndCheckInterrupt(),用于将线程B挂起
iii)第三块,若failed==true,表明线程B不愿意再等待,直接退出排队
unlock()
流程:
sync.release()=>tryRelease(arg)=>unparkSuccessor=>杀回马枪
i)
ii)将哨兵节点的waitStatus由0置为-1
将哨兵节点的下一个节点唤醒unpark()
iii)杀回马枪
将哨兵节点的指针指向去掉,这样它就被GC了。哨兵节点紧接着的节点,也就是抢占了之前线程A位置的节点,变成了哨兵节点
最后
三大流程走向