Synchronized与AQS对比
Synchronized底层的同步队列为cxq和entryList, 等待队列为waitSet
AQS 同步队列为CLH, 条件队列为Condition
方法类比:
Synchronized | AQS | |
---|---|---|
wait | await | |
notify | signal | |
nofifyAll | signalAll |
方法作用类比:
wait : 把当前线程放入waitSet,调用park
await: 把当前线程放入条件队列,调用park
notify: 从waitSet中哪一个线程等待对象出来放入entrySet或者cxq。
signal: 从条件队列的队头里把一个node节点放入CLH队列末尾。
注意:notify和signal 都是不会唤醒线程的,他们只把线程放入队列里面,等待其他线程释放锁的时候被唤醒, 同样的interrupt并不会真正中断线程,相反 interrupt只是设置了一个线程中断标志位为1,然后unpark唤醒当前线程。所以要处理中断,还得业务对这个中断标识进行判断。
条件队列示意图
呃, CLH队列是双向的, 条件队列是单向的,切状态为conidtion,
wait 和 await 、notify和signal 都必须在同步代码块中间, 即都要在获取锁和释放锁代码中间, Synchronized只有一个waitSet, 而condition队列可以有多个
public class SemaphoreTest {
private static ReentrantLock reentrantLock = new ReentrantLock();
private static Object moniter = new Object();
public static void main(String[] args) throws InterruptedException {
/*synchronized*/
synchronized (moniter) {
moniter.wait();
moniter.notify();
}
/*condidion*/
Condition condition = reentrantLock.newCondition();
reentrantLock.lock();
condition.await();
condition.notify();
reentrantLock.unlock();
}
}
await() 响应中断
首先要明确一点, 任何线程同一个时刻只会存在一个队列中, 而且执行await的方法的线程首先是要获取锁
await的具体方法逻辑三步:
第一步:进入条件队列
第二部:释放锁
第三步:阻塞
第四步:中断补偿
其中还需要特别注意的是, 第一步到第二步之间,在没有释放锁之前,因为是独占锁,所以这个期间是只有当前线程在执行,所以没有线程安全问题,所以不需要考虑多线程执行,什么cas补偿的, 但是释放锁之后 开始别的线程就可以获取锁了,那么就要以多线程思维来考虑问题
进入条件队列方法 addConditionWaiter
条件队列如上图所示, 这里还需要看看条件队列的类结构:
/*条件队列*/
public class ConditionObject implements Condition {
private volatile Node firstWaiter;/*头指针*/
private volatile Node lastWaiter;/*尾指针*/
private static final int REINTERRUPT = 1;/*中断需要补偿*/
private static final int THROW_IE = -1;/*中断需要抛出中断异常*/
}
private Node addConditionWaiter() {
Node t = lastWaiter; /*尾节点添加*/
// If lastWaiter is cancelled, clean out. 如果尾节点的状态是取消
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters(); /*把当前的条件队列的所有CANCLE节点都拿掉*/
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION); /*组装当前要挂起的节点*/
if (t == null)/*还没有初始化*/
firstWaiter = node;
else/*已经初始化了*/
t.nextWaiter = node;
lastWaiter = node; /*尾指针指向*/
return node;
}
呃, 进入条件队列的方法整体逻辑还是比较容易想通的, 里面有个点需要进行多思考的地方就是if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); /*把当前的条件队列的所有CANCLE节点都拿掉*/ t = lastWaiter; }
, 呃这个段代码的为啥这样写。。。。。, 这里需要明确一个问题条件队列和同步队列的状态有什么区别,或者说是条件队列中的节点状态有那些,同步队列中的状态又有那些
static final int CANCELLED = 1; /*同步/条件队列 线程已取消 发生了异常 */
static final int SIGNAL = -1; /同步队列 后继的线程需要被唤醒/
static final int CONDITION = -2; /*条件队列 */
static final int PROPAGATE = -3; /同步队列 共享锁/
可以看出条件队列节点的状态要么是 取消 要么是condition
哪里会使得条件队列里面的节点状态变成 CANCELLED
获取锁释放锁的时候 模板方法,tryAcquire 和tryRelease都是交给子类去实现的,所以在这个两个方法抛出异常之后,说明这个这个两个节点都需要状态变为取消。
去掉已经取消的节点 unlinkCancelledWaiters
这里说实话可以出来一个疑问?就是为啥不在出异常的时候就把节点取消掉,而在是下一个节点入队的时候从头遍历处理。。。,呃,我理解如果是为了批处理感觉说法不够有理由,我觉得应该是一个比较好的习惯。
private void unlinkCancelledWaiters() {
Node t = firstWaiter;/*获取头节点*/
Node trail = null; /*记录前驱非CANCLE节点的位置*/
while (t != null) {
Node next = t.nextWaiter; /*获取下一个节点*/
if (t.waitStatus != Node.CONDITION) { /*条件队列里面节点的类型 要么是 CANCLE要么是CONDITION*/
t.nextWaiter = null; /*断开该CANCLE节点的nextWaiter指向*/
if (trail == null) /*说明该CANCLE节点前面没有 CONDITION节点*/
firstWaiter = next; /*直接把头节点指向next就行*/
else /*说明前面有condition的节点 需要进行跨越 该cancle节点*/
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail; /*到队列末尾了*/
}
else
trail = t;
t = next;
}
}
完全的释放锁 fullyRelease
释放的有可能是重入的,都要把state设置为0
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState(); /*获取state值*/
if (release(savedState)) { /*释放*/
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED; /*释放有异常*/
}
}
呃, 由上可见 如果释放锁release方法有异常, 节点的取消状态此时被设置上去了, 然后异常接着往外抛出去,外层的finally里面的unlock最终会释放锁,唤醒CLH同步队列的节点。
阻塞
呃 ,这里说实话有点难理解。代码也不好看。直接说一下我理解的思路吧。。。。
阻塞调用的肯定是Unsafe的底层native方法 park, 呃这方法是可以由2种方法被唤醒, 第一种就是unpark , 第二种就线程中断(线程中断底层调用的是c++的unpark操作系统级别的api)
所以说park有那些情况会被唤醒:
1、用户自己调用unpark ; 2、signal也是调用的unpark ; 3、用户中断线程
觉得那种不合理呢。。。。。, 第一种方式不合理: 如果是用户unpark自己唤醒的话, 我们需要一个循环机制继续park住, 那怎么设计这个循环机制的while条件呢?
呃, 那用户自己使用和signal的区别呢?
区别就是在于用户自己unpark, 我们是不能确定是否已经获取锁了, 应为只有获取锁的线程才能唤醒别的线程(呃,这里也不是唤醒,虽然交唤醒), 所以signal才是合法的api,他会判断当前线程是不是aqs独占锁的持有者,这样当前线程才能有能力去signal。
signal的方法逻辑 : 大概就是 需要判断条件队列节点有,那么获取第一个,然后cas修改node节点的状态从condition到0, 修改成功之后需要把node从条件队列摘除,移动到CLH队列的末尾也就是入队, 那么入队之后我们需要判断他在CLH队列中的前驱节点的状态是不是SIGNAL或者我们能把他修改成SIGNAL,因为 await之后的node的节点所代表的线程最终是应该park,所以如果前驱不是SIGNAL状态,需要唤醒这个入队的node节点代表的线程。
呃,所以这里就可以看出来 , 循环条件应该使用 判断节点是不是已经移动到了同步队列,因为用户自己unpark的线程,是不会把node节点从条件队列移动到CLH队列。
这里为什么说应该呢,诶, 想一个释放锁之后才去park的, 中间有可能有个线程已经获取到锁了, 然后signal,signal会把改节点移动到CLH同步队列,这个使用循环条件判断已经入队了,就不会去走park。
呃呃呃, 如果是用户中断呢。 用户中断也是合理的,所以我们需要对中断进行判断。。具体看代码逻辑,大概逻辑就是用户中断和别的线程signal都会去cas修改这个条件队列node节点的状态从 condition到0, 然后修改成功的就node节点进入CLH队列。
public void await() throws InterruptedException {
/*检查是否线程已经中断了*/
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
/*包装当前线程为条件队列的node, 入队*/
Node node = addConditonWaiter();
/*释放锁(可能是重入的)*/
int saveState = fullRelease(node);
/*中断标志*/
int interruptMode = 0;
/*阻塞 如果node不在同步队列里面就进行阻塞*/
while (!isOnSyncQueue(node)){
LockSupport.park();
/*判断是什么引起的被唤醒, 用户自己unpark? signal ? 用户中断?
* 如果是用户unpark 我们得如果不进入同步队列,还是需要继续park住。
* 如果是Signal唤醒 node已经入同步队列,所以while条件就会退出来
* 如果是中断,呃应为设计的是响应中断,所以也要break掉。
* 注意 : 中断 和 signal两者都需要去修改node的状态从CONDITION到0
* */
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break; //发生中断 , interruptMode为 -1说明中断已经cas node的节点状态抢过了 signal。 1 则是没有抢过。
}
/*到这里说明已经进入同步队列,且被唤醒了, 得继续去抢锁*/
if (acquireQueue(node, saveState) && interruptMode != ConditionObject.THROW_IE){
interruptMode = REINTERRUPT; //这里补偿的中断是acquireQueue里面的中断
}
/*已经拿到锁了*/
if (node.nextWaiter != null){//说明是interrupte, 如果是SIGNAL的话,nextWaiter是会被置为Null的。
//清理清理条件队列中的CANCELLED节点
unlindCancelledWaiters();
}
//检查中断模式
if (interruptMode != 0){
reportInterruptAfterWait(interruptMode);
}
}
判断node有没有在CLH同步队列 isOnSyncQueue
/*判断当前节点是否在同步队列上*/
private boolean isOnSyncQueue(Node node) {
if (node.waitState == Node.CONDITON || node.prev == null){ //条件队列node节点只有状态为CONDITION和nextWaiter指向下一`在这里插入代码片`个节点别的都为null
return false;
}
if (node.next != null){
return false;
}
return findNodeFromTail(node); //从后往前找
}
这个要注意多思考一下:node.next为空的情况,就是cas 入队CLH队列末尾的时候的入队逻辑。所以signal唤醒入队的话。会存在node已经入队了tail也指向node了,但是node前驱的next还没有来得即指向node。
这个使用从tail往前找就可以找到:
findNodeFromTail
private boolean findNodeFromTail(Node node) {
Node t = this.tail;
for (;;){
if (t == null) { //找到头了就说明没有入队
return false;
}
if (t.thread == node.thread){//找到了node里面的线程都是同一个说明是用一个node
return true;
}
t = t.prev;
}
}
判断是中断唤醒的park还是signal唤醒的park checkInterruptWhileWaiting
/*检查是不是被中断了*/
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) ://中断 但是signal是有可能一起执行的
0; //说明是用户自己unpark或者signal
}
中断与signal竞争 transferAfterCancelledWait
/*中断需要干啥,需要把node节点的状态从condition修改到0*/
private boolean transferAfterCancelledWait(Node node) {
/*如果中断cas竞争过了 signal*/
if (compareAndSetWaitState(node, Node.CONDITON, 0)){
casEnqueue(node); /*入队*/
return true;
}
/*没有竞争过, 那就等着signal把node从条件队列移动到同步队列*/
while (!isOnSyncQueue(node)){
/*礼让给 SIGNAL的线程*/
Thread.yield();
}
/*此次中断失效,需要进行补偿, 没有抢过signal*/
return false;
}
中断唤醒与 signal唤醒的区别
呃呃呃呃, 他们都会把节点移动到CLH队列里面,但是signal会把nextWaiter置为null, 中断没有。所以如果是中断唤醒的,我们需要清理一下CANCELLED的线程,哈哈哈,这里也说明了节点状态为0也会存在条件队列中(CLH入队,node节点的状态为0)。
中断补偿或者抛出异常reportInterruptAfterWait
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
if (interruptMode == THROW_IE){ /*直接抛出,代表interrupte在signal之前完成*/
throw new InterruptedException();
}else if (interruptMode == REINTERRUPT){ /*中断位补偿,说明signal抢在interrupte前面*/
Thread.currentThread().interrupt();
}
}
signal
public void signal() {
/*当前线程不是锁的拥有者*/
if (getOwnThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException();
}
Node first = firstWaiter; //获取条件队列第一个线程node节点
if (first != null) {//有可以移动的节点
doSignal(first); //开始尝试移动
}
}
doSignal
private void doSignal(Node first) {
/*这里为啥使用while循环, 因为signal如果抢不过用户中断唤醒node的话,我们需要继续signal
* signal代表的意思是别的线程要释放锁了,可以把条件队列的线程移动一个到CLH准备等待自己释放
* 锁然后唤醒。 所以signal必须唤醒一个线程(除非条件队列为空了), 这样能避免造成死锁
* */
do {
/*队列中只有一个节点*/
if ((firstWaiter = first.nextWaiter) == null) {
lastWaiter = null;
}
first.nextWaiter = null;/*从条件队列中断开, 这里和中断的区别,中断不会把nextWaiter置为null*/
} while (!transForSignal(first) && (first = firstWaiter) != null);//挪动节点,如果挪动失败(挪动失败是因为中断cas竞争修改节点状态成功了) 换下一个节点。
}
与中断竞争 transForSignal
/*挪动节点到同步队列*/
private boolean transForSignal(Node node) {
if (!compareAndSetWaitState(node, Node.CONDITON, 0)) {//interrupt signal 两者竞争
return false; //失败,挪动失败
}
/*状态改成功, 需要进行入队*/
Node p = casEnqueue(node); //返回的p是node在CLH队列里面的前驱节点
int waitState = p.waitState;
if (waitState > 0 || !compareAndSetWaitState(p, waitState, Node.SINGAL)) {//如果前驱节点的状态是失效,或者cas不把状态修改为SIGNAL
LockSupport.unpark(node.thread);/*前驱节点没有能力去唤醒node,需要线程就唤醒node*/
}
return true; //挪动成功
}
await不响应中断 awaitUninterruptibly
/*不响应中断*/
@Override
public void awaitUninterruptibly() {
//入队--条件队列
Node node = addConditonWaiter();
//释放锁 -- state
int saveState = fullRelease(node);
//阻塞
boolean isInterrupt = false;
while (!isOnSyncQueue(node)){
LockSupport.park();
if (Thread.interrupted()){
isInterrupt = true;
}
}
//走到这里说明是有线程把node从条件队列移动到CLH,等待被唤醒
//获取锁 这里有两种中断 acquireQueue里面的中断 和 上面while循环里面的中断
if (acquireQueue(node, saveState) || isInterrupt){
Thread.currentThread().interrupt();//补偿中断
}
}
不响应中断的意思就是 , 用户中断操作也不能唤醒在条件队列的node节点,但是中断状态还是正常补偿的,因为线程中断,park是不会生效了,就会一直循环,造成cpu浪费,所以需要消除中断,然后最后被signal唤醒的时候进行补充, 当然这里的补充包括2种, 因为有两个地方park, 一个是while里面, 一个是acquireQueue里面
超时await自动唤醒 awaitNanos
呃,这个功能的时候在于park的参数如果不为0,是可以指定阻塞时间,到了时间就自己动唤醒, 但是有一个点还是需要注意, 自己到时间唤醒之后,需要自己把自己移动到CLH队列,等待别的线程唤醒,因为是互斥锁, 你到点自己唤醒之后,AQS的锁是别的线程拥有着的, 你必须把自己送入到CLH队列,然后去尝试获取锁,这样才能互斥
public long awaitNanos(long nanosTimeout) throws InterruptedException {
/*检查是否线程已经中断了*/
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
/*包装当前线程为条件队列的node, 入队*/
Node node = addConditonWaiter();
/*释放锁(可能是重入的)*/
int saveState = fullRelease(node);
/*中断标志*/
int interruptMode = 0;
/*最长的阻塞时间 绝对时间*/
long deadLine = System.nanoTime() + nanosTimeout;
/*阻塞 如果node不在同步队列里面就进行阻塞*/
while (!isOnSyncQueue(node)){
if (nanosTimeout <= 0){/*说明时间到了*/
/*自己把自己移动到CLH*/
transferAfterCancelledWait(node);
break;
}
//不用阻塞了, 时间太小了
if (deadLine >= spinForTimeoutThreshold){
LockSupport.parkNanos(this, nanosTimeout);
}
/*判断是什么引起的被唤醒, 用户自己unpark? signal ? 用户中断?
* 如果是用户unpark 我们得如果不进入同步队列,还是需要继续park住。
* 如果是Signal唤醒 node已经入同步队列,所以while条件就会退出来
* 如果是中断,呃应为设计的是响应中断,所以也要break掉。
* 注意 : 中断 和 signal两者都需要去修改node的状态从CONDITION到0
* */
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break; //发生中断 , interruptMode为 -1说明中断已经cas node的节点状态抢过了 signal。 1 则是没有抢过。
nanosTimeout = deadLine - System.nanoTime();
}
/*到这里说明已经进入同步队列,且被唤醒了, 得继续去抢锁*/
if (acquireQueue(node, saveState) && interruptMode != ConditionObject.THROW_IE){
interruptMode = REINTERRUPT; //这里补偿的中断是acquireQueue里面的中断
}
/*已经拿到锁了*/
if (node.nextWaiter != null){//说明是interrupte, 如果是SIGNAL的话,nextWaiter是会被置为Null的。
//清理清理条件队列中的CANCELLED节点
unlindCancelledWaiters();
}
//检查中断模式
if (interruptMode != 0){
reportInterruptAfterWait(interruptMode);
}
return 0;
}
总结
呃, 说实话真的挺难的。。。。。。,一定要注意那些代码是多线程执行, 那些代码逻辑是单线程执行(注:await() 响应中断)。
还有一个地方: 就是响应中断:对于一个node节点的唤醒2中情况 如果中断 cas竞争过了signal,那么需要抛出中断异常, 如果signal cas竞争过了中断,需要补充中断标志, 对于中断与signal对同一个节点的竞争,signal的while循环,中断竞争失败 Thread.yield礼让cpu给 signal去使用去移送node到CLH , 这些细节点 真的挺难的,这谁能想的到这种。。。思想。 李老爷子真的是世界级并发编程大师