概述
通过之前的分析,深入了解了AbstractQueuedSynchronizer的内部结构和一些设计理念,知道了AbstractQueuedSynchronizer内部维护了一个同步状态和两个排队区,这两个排队区分别是同步队列和条件队列。
拿ATM机取款举例,ATM机如下图所示:
同步队列是主要的排队区,如果ATM机没开放,所有想要进入人都得在这里排队。而条件队列主要是为条件等待设置的,想象一下如果一个人通过排队终于成功获取锁进入了ATM机,但在取款之前发现自己没带银行卡,碰到这种情况虽然很无奈,但是它也必须接受这个事实,这时它只好乖乖的出去先准备好银行卡(进入条件队列等待),当然在出去之前还得把锁给释放了好让其他人能够进来,在准备好了银行卡(条件满足)之后它又得重新回到同步队列中去排队。当然进入房间的人并不都是因为没带银行卡,可能还有其他一些原因必须中断操作先去条件队列中去排队,所以条件队列可以有多个,依不同的等待条件而设置不同的条件队列。
同步队列和条件队列的区别:
- 同步队列的头结点为head,而条件队列的头结点为firstWaiter;
- 同步队列的尾结点为tail,而条件队列的尾结点为lastWaiter;
- 同步队列的头结点没有和任何线程绑定,而条件队列的firstWaiter绑定了线程。
- 同步队列是一条双向链表,而条件队列是一条单向链表。
Condition接口定义了条件队列中的所有操作,AbstractQueuedSynchronizer内部的ConditionObject类实现了Condition接口,下面看看Condition接口都定义了哪些操作。
1 | public interface Condition { |
Condition接口虽然定义了这么多方法,但总共就分为两类,以await开头的是线程进入条件队列等待的方法,以signal开头的是将条件队列中的线程“唤醒”的方法。
需要注意的是,调用signal方法可能唤醒线程也可能不会唤醒线程,什么时候会唤醒线程这得看情况,但是调用signal方法一定会将线程从条件队列中移到同步队列尾部。
await方法分为5种,分别是响应线程中断等待,不响应线程中断等待,设置相对时间不自旋等待,设置相对时间自旋等待,设置绝对时间等待;
signal方法只有2种,分别是只唤醒条件队列头结点和唤醒条件队列所有结点的操作。同一类的方法基本上是相通的。
响应线程中断的条件等待
1 | //响应线程中断的条件等待 |
上述代码整个流程总结如下:
第一步:首先判断当前线程是否中断,如果被中断,则抛出异常,如果没有被中断,则继续下面的流程。
第二步:通过调用addConditionWaiter()将当前线程封装成Node节点存放到Condition队列的尾部。
第三步:因为当前线程已经获取了锁,所以调用await需要释放资源,所以通过调用fullyRelease()释放资源,也就是释放锁,因为这个锁是独占锁并且可以重入,所以要全部把资源释放,从fully字面上也可以理解。
第四步:通过while循环判断当前线程是否在同步队列上,如果没有在同步队列上,则需要阻塞当前线程,然后调用checkInterruptWaiting()方法判断是否被中断过,如果被中断过,则跳出while循环。
第五步:通过调用acquireQueued()方法获取资源,如果在调用这个方法时被中断,则中断类型变成REINTERRUPT(稍后处理中断),这个方法返回值只是记录是否被中断过,并不会响应中断。
第六步:如果是因为中断,此时waitStatus=0,但是此时它仍在条件队列中,所以需要从条件队列中清除。
第七步:如果被中断,则调用reportInterruptAfterWait()方法处理不同的中断类型。
第一步:将线程添加到条件队列
1 | private Node addConditionWaiter() { |
将当前线程封装成Node节点,然后加入到Condition的尾部,在加入之前需要检查以下尾部节点t是否还在等待Condition条件,如果被signal或者被中断,则调用清除方法将尾节点从Condition队列中清除掉。
第二步:完全将锁释放
1 | //完全释放锁 |
将当前线程包装成结点添加到条件队列尾部后,紧接着就调用fullyRelease方法释放锁。注意,方法名为fullyRelease也就这步操作会完全的释放锁,因为锁是可重入的,所以在进行条件等待前需要将锁全部释放了,不然的话别人就获取不了锁了。如果释放锁失败的话就会抛出一个运行时异常,如果成功释放了锁的话就返回之前的同步状态。
第三步:进行条件等待
1 | //线程一直在while循环里进行条件等待 |
在以上两个操作完成了之后就会进入while循环,可以看到while循环里面首先调用LockSupport.park(this)将线程挂起了,所以线程就会一直在这里阻塞。在调用signal方法后仅仅只是将结点从条件队列转移到同步队列中去,至于会不会唤醒线程需要看情况。如果转移结点时发现同步队列中的前驱结点已取消,或者是更新前驱结点的状态为SIGNAL失败,这两种情况都会立即唤醒线程,否则的话在signal方法结束时就不会去唤醒已在同步队列中的线程,而是等到它的前驱结点来唤醒。当然,线程阻塞在这里除了可以调用signal方法唤醒之外,线程还可以响应中断,如果线程在这里收到中断请求就会继续往下执行。可以看到线程醒来后会马上检查是否是由于中断唤醒的还是通过signal方法唤醒的,如果是因为中断唤醒的同样会将这个结点转移到同步队列中去,只不过是通过调用transferAfterCancelledWait方法来实现的。最后执行完这一步之后就会返回中断情况并跳出while循环。
第四步:结点移出条件队列
1 | //线程醒来后就会以独占模式获取锁 |
当线程终止了while循环也就是条件等待后,就会回到同步队列中。不管是因为调用signal方法回去的还是因为线程中断导致的,结点最终都会在同步队列中。这时就会调用acquireQueued方法执行在同步队列中获取锁的操作。也就是说,结点从条件队列出来后又是乖乖的走独占模式下获取锁的那一套,等这个结点再次获得锁之后,就会调用reportInterruptAfterWait方法来根据这期间的中断情况做出相应的响应。如果中断发生在signal方法之前,interruptMode就为THROW_IE,再次获得锁后就抛出异常;如果中断发生在signal方法之后,interruptMode就为REINTERRUPT,再次获得锁后就重新中断。
不响应线程中断的条件等待
1 | //不响应线程中断的条件等待 |
设置相对时间的条件等待(不进行自旋)
1 | //设置定时条件等待(相对时间), 不进行自旋等待 |
设置相对时间的条件等待(进行自旋)
1 | //设置定时条件等待(相对时间), 进行自旋等待 |
设置绝对时间的条件等待
1 | //设置定时条件等待(绝对时间) |
唤醒条件队列中的头结点
1 | //唤醒条件队列中的下一个结点 |
可以看到signal方法最终的核心就是去调用transferForSignal方法,在transferForSignal方法中首先会用CAS操作将结点的状态从CONDITION设置为0,然后再调用enq方法将该结点添加到同步队列尾部。我们再看到接下来的if判断语句,这个判断语句主要是用来判断什么时候会去唤醒线程,出现这两种情况就会立即唤醒线程,一种是当发现前驱结点的状态是取消状态时,还有一种是更新前驱结点的状态失败时。这两种情况都会马上去唤醒线程,否则的话就仅仅只是将结点从条件队列中转移到同步队列中就完了,而不会立马去唤醒结点中的线程。signalAll方法也大致类似,只不过它是去循环遍历条件队列中的所有结点,并将它们转移到同步队列,转移结点的方法也还是调用transferForSignal方法。
唤醒条件队列的所有结点
1 | //唤醒条件队列后面的全部结点 |