在本文,来认识JUC包提供的AbstractQueuedSynchronized(AQS). AQS是一个构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效的构造出来, 不仅ReentrantLock和Semphore是基于AQS构建的,还包括CountDownLatch,ReentrantReadWriteLock、SynchronousQueue和FutureTask等。 (摘自JUC并发编程)
接下来就带着下面几个疑问出发去认识JUC包下面的AbstractQueuedSynchronizer.(以下简称AQS)
1. AQS都有哪些成员变量,哪些方法,对外提供了些什么功能
2. 为什么大多数的锁和同步器都是将操作委托给AbstractQueuedSynchronizer子类实现,而不是直接实现AQS中的方法.(采用委托实现而不是继承实现)
3. ReentrantLock fair/no-fair模式都是怎么实现的,主要着重点在于公平和非公平特性上
4. 其他同步类 CountDownLatch/CyclicBarrier/Smphore/ReentrantReadWriteLock/
SynchronousQueue/FutureTask都是怎么样借助AQS来实现各自的功能的.
StampedLock(jdk 1.8加入的)又为什么没有采用AQS来实现了?
5. 像ReentrantLock中的可重入、可中断、公平/非公平、超时等特性是怎么实现的
6. AQS做了哪些事情
7. 在线程的状态转换中, AQS在阻塞等待时为什么线程是Waiting的,而不是Blocked
8. AQS中阻塞都是怎么实现的, while循环阻塞还是其他的什么方式
9. ReentrantLock是独占锁模式,像Semphore这样的共享锁模式又是怎样实现的
这里简单的列举了一些可能比较常见的疑问(这里是我在阅读AQS之前产生的疑问), 在阅读过程中也可能产生一些疑问, 让我们带着问题去认识JUC提供的AQS吧!
10. Lock+Condition显式条件队列是怎么实现的, 和synchornized+内置条件队列的比较
1. AQS成员变量和方法
先上一张AQS相关类的类图
1. exclusiveOwnerThread
我们发现AQS并不是一个顶级抽象类接口,其实是还有个AbstractOwnableSynchronizer的, 在AbstractOwnerSynchronizer中发现了一个成员变量exclusiveOwnerThread 用来记录在独占锁模式下是哪个线程占用了锁.
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable
{
/** Use serial ID even though all fields transient. */
private static final long serialVersionUID = 3737899427754241961L;
/**
* Empty constructor for use by subclasses.
*/
protected AbstractOwnableSynchronizer() { }
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread; // other ...}
并且提供相应的访问方法
在AQS父类AbstractOwnerSynchornizer中通过exclusiveOwnerThread成员变量记录独占锁下持有锁的当前线程, 为后面ReentrantLock的可重入特性实现提供了基础.
2. state
在AQS类中,还有个volatile成员变量state , 用来记录同步状态, 通过setState/getState以及compareAndSetState来对其进行访问。对于state成员变量的线程同步安全的保证采用的是CAS+volatile方式来证的。 state变量在不同的同步器中有不同的用法, ReentrantLock中用(state=0)表示没有加锁, (state=1+)的方式表示已加锁并记录加锁次数; 在Semphore中用来记录许可证数量, 获取许可成功减1,释放许可加1;在ReentrantReadWriteLock中, 将state的二进制高位16 bit用来记录readLock数量, 低位16 bit记录写锁。
3. 对外访问方法
(exclusive-mode)
acquire tryAcquire
acquireInterruptibly
tryAcquireNanos
release tryRelease
(share-mode)
acquireShared tryAcqureShared
acquireSharedInterruptibly
tryAcquireSharedNanos
realseShared tryReleaseShared
Note: 左边是AQS对外提供的公共方法(模板方法), 右边是子类中需要实现的部分.
在这些方法中提供了可中断获取的方式acquireInterruptibly, 也提供了超时获取的方式tryAcquireNanos.
4. 回顾上面的AQS的类图, 可以看到在AQS中有个内部类ConditionObject实现了Condition接口提供了显式条件队列的实现。 还可以看到里面有个内部类Node, 这是用来实现同步等待队列和条件等待队列的.
可以看到在AQS中有head/tail这两个Node变量的, 可以猜测同步等待队列是个双端链表.
(这里可以先跳到第四节ReentrantLock先看怎么实现的,再看下面的这2节)
2. 基于AQS的同步器为什么不是直接继承
基于AQS实现的同步器,为什么不是通过拓展AQS(继承)的方式而是将功能委托给内部AQS实现类来实现?
Answer: 这样实现有很多原因, 最主要的原因是拓展AQS的方式会破坏Lock这样接口的简洁性, 本来只需要看到Lock提供的lock/release就知道怎么使用了;如果拓展AQS之后,就可以访问到AQS中提供的一些公共方法了, 在编程上不是很有好, 都不知道哪个是我要调用的方法了。 虽然AQS的公共方法不允许调用者破坏闭锁的状态,但是调用者还仍可以很容易的误调用它们。而将使用委托的方式就比较简单了, 我能看到的之有lock/release方式,也很快就知道了哪个是加锁,哪个是释放锁,该在什么时候调用它们.
3. 使用AQS有什么好处
AQS解决了在实现同步器是涉及的大量细节问题,例如等待线程采用FIFO队列操作顺序。在不同的同步器中还可以定义一些灵活的标准:某个线程是等待还是通过.
基于AQS来构建同步器能带来许多好处。它不仅能极大减少实现工作,而且不必处理多个位置上发生的竞争问题(这里在没有使用AQS来构建同步器的情况)
在JDK中是否有这样的情况,或者没有使AQS实现的同步器是怎么样考虑的.
在<JUC并发编程>中SemaphoreOnLock可以解释一下使用AQS的好处: 在SemaphoreOnLock中, 获取许可的操作可能在两个时刻阻塞, 当锁保护信号状态时以及当许可不可用时。 在基于AQS构建的同步器(Semaphore)中,只可能在一个时刻发生阻塞: (许可不可用时), 从而降低了上下文的开销并提高了吞吐量。
在设计AQS时充分考虑了可伸缩性,因此java.util.concurrent包中所有基于AQS构建的同步器都能获得这个优势.
(Ps: StampedLock也没有采用AQS实现,是基于什么考虑??)
4. ReentrantLock
前面大致了解了一下AQS有哪些成员变量,又提供了哪些访问方法, 接下来我们就从ReentrantLock来看看是怎么实现同步器需要的功能的,同时回答前面提出的问题。
ReentrantLock.lock整体流程
跟踪ReentrantLock.lock的调用链,发现fair/non-fair模式基本相同
Fair: ReentrantLock.lock -> sync.lock(FairSync) -> AQS.acquire -> FairSync.tryAcquire
Non-fair: ReentrantLock.lock ->sync.lock(NonFairSync) -> AQS.acquire -> NonFairSync.tryAcquire -> NonFairSync.nonfairTryAcquire
fair/non-fair不同的地方就在于Sync需要实现AQS中tryAcquire方法不一样。
在ReentrantLock中不管是fair/non-fair中lock方法都是委托给AQS.acquire方法实现的.
这里先介绍一下acquire的基本流程:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire都是先tryAcquire尝试获取一下锁,如果获取失败, 将当前线程节点(Node)加入到队列中addWaiter, 然后排队尝试获取acquireQueued.
1. tryAcquire
Non-fair模式实现细节: NonFairSync.tryAcquire -> Sync.nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
Fair模式实现细节: FairSync.tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
1. fair/non-fair模式就多了一个判断!hasQueuedPredecessors(), 用来公平性判断,这里在后面
2. tryAcquire获取锁的基本流程是,先判断锁状态 state==0没有加锁,就是用compareAndSetState(0,acquires)来尝试加锁,加锁成功设置exclusiveOwnerThread值记录获取锁记录的线程; 如果state!=0 && current == getExclusiveOwnerThread: (已加锁, 但是锁记录的所有者是当前线程)那么就可重入, 锁记录次数state+acquires(1)。
如果加锁失败,就进入后面的入队了等待流程:
acquireQueued(addWaiter(Node.exclusive),arg)
2. 入队列addWaiter
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
在上面可以看到当前线程竞争锁失败,是以当前线程thread构建一个Node节点放入headt/tail双端链表构建的一个队列中. 在开始的时候判断(pred=tail) !=null ,发现队列不为空,就尝试快速入队列; 如果tail==null(说明队列还没有构建,使用的延迟初始化)或者快速入队列失败, 则进入for(;;) + compareAndSetTail 循环入队列的过程,直到Node节点入队列成功。
这里延迟初始化队列(compareAndSetHead)和入队列操作(compareAndSetTail)都是一个Cas操作, 同时head和tail也是volatile变量, CAS+volatile +for(;;)就实现了一个轻量级的自旋锁, 保证了多线程并发时候的队列延迟初始化和节点入队列的线程安全。
关于enq(node)中入队列的细节图解, (刚开始看到这里compareAndSetTail(t,node)的时候有点疑惑)
3. acquireQueued 入队列获取
在第一次tryAcquire尝试获取锁失败后,就将当前线程入等待队列(head/tail 维护)。 入等待队列后, 就是一个在其中等待获取的过程。
acquireQueued实现细节:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以看到是使用的for(;;) + tryAcquire的方式在排队等待获取锁, 一直等待获取锁成功. (当然不是这么简单haha,这里只是简单描述一下这个尝试获取的过程,这次获取不到等下一次再来获取)
4. ReentrantLock公平性/非公平
Question: ReentrantLock中fair/non-fair模式是怎么样实现的?
公平性: 等待获取锁的线程排队, 先到的先的, 而不是抢线程
非公平: 抢占式的获取锁
在fair/non-fair的比较中, fair/non-fair相比就多了一个!hasQueuedPredecessors()判断
non-fair模式没有!hasQueuedPredecessors()的判断。
但是前面acquireQueued一直尝试获取锁的过程中, 有这样的代码:
在再次尝试获取锁之前,先判断该线程等待节点node是不是head之后的第一个有效节点,这样是不是在fair模式中!hasQueuedPredecessor()判断该线程前是否还有等待的节点就没有作用了?反正都是该线程节点要是队列中第一个有效节点才能尝试获取锁。
hasQueuedPredecessors()
hasQueuedPredecessors()是用来判断当前线程前面是否还有队列在排队等待,当等待队列为空或者当前线程是等待队列中的第一个有效节点的时候返回false.
当在fair模式下, 当前线程在第一次tryAcquire的时候, hasQueuedPredecessors()返回true(因为当前线程还没有入队列), 所以结果就是fair模式第一次tryAcquire的结果就是false. 然后开始入队列模式,在队列中等待排队获取锁。
所以,non-fair模式在线程第一次tryAcquire的时候会有一次竞争锁的机会, 这个时候如果有多个线程都是第一次进行tryAcquire操作, 那么就是谁的compareAndSetState(0,acquires)操作成功就是那个线程获取到锁, 这就是抢占是的获取锁。
在fair模式中tryAcquire添加了!hasQueuedPredecessors()判断, 在线程第一次tryAcquire的时候因为当前线程还没有入队列, !hasQueuedPredecessors()的值为false, 所以第一次tryAcquire其实没有参与锁竞争操作(compareAndSetState), 线程都是乖乖的入队列(addWaiter),然后在acquireQueued中排队获取锁记录。
对p==head && tryAcquire(arg)的代码块的解释:
在acquireQueued中, 只有当前线程节点是head(头节点)下的第一个节点才有机会去尝试获取锁, 所以在addWaiter(入队列)的时候先到的线程先得到tryAcquire的机会。 当再次tryAcquire(线程第二次执行tryAcquire的机会)的时候, 因为当前线程节点已入队列而且是第一个队列节点, 所以 !hasQueuedPredecessors() 的值是true,就接下来可以compareAndSetState来获取锁记录了.
5. ReentrantLock的阻塞
Question: 使用ReentrantLock.lock时, 线程如果没有竞争到锁,对线程的阻塞体现在哪里?
Answer: 在等待获取锁的时候,难道真的是for(;;) + tryAcquire的方式来实现的么。肯定不是这么简单的, 如果只是简单的for(;;) + tryAcquire那就是一个自旋锁了, 也没让出CPU执行空间。肯定是还有其他操作来实现这个等待操作, 兼顾等待和CPU开销。
在acquireQueued中,可以看到在获取锁失败后,有一段代码判断在失败之后是否应该等待, 那么就来看看什么时候该等待,以及怎么阻塞等待的.
shouldParkAfterFailedAcqure(p,node)
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
在判断是否应该阻塞等待的时候,需要用到Node节点的waitStatus, 先来看看线程节点入队列的时候是什么值。
waitStatus
在addWait(Node.EXCLUSIVE)中是使用的new Node(Thread.currentThread,mode)方式构造的当前线程节点
跳到构造方法Node(thread,mode), 可以看到在构造的时候没有设置waitStatus值, 那只能是默认值0
在对Node.waitStatus字段的解释也是默认值设定的为0
何时等待
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
附上Node.waitStatus枚举值
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
可以看到在node的prev.waitStatus == Node.SIGNAL被标记为需要唤醒后,当前节点node代表的线程就可以等待了; 那么prev节点在什么时候waitStatus被设置为Node.SIGNAL? 当prev节点waitStatus==0 || Node.PROPAGATE的时候被设置为Node.SIGNAL。
所以这里会有2次p==head && tryAcquire的机会, 只有2次都没有获取到锁,我们才能确认当前线程还获取不到锁, 需要等待. (第一次p==head && tryAcquire失败, 只是标记为Node.SIGNAL, 第二次p==head && tryAcquire失败才会发现prev.waitStatus == Node.SIGNAL, 才需要等待)
Question:为什么这里通过判定prev.waitStatus的值和标记prev.waitStatus的值来判断node所代表的线程是否需要等待, 而不是直接设置node.waitStatus?
Answer: 使用node.prev表示当前线程节点被阻塞也能理解,意思代表前置线程节点需要被唤醒,需要等待前面的线程节点获取到锁之后才能让当前线程节点拿到锁, 至于为什么不是用线程自己节点node.waitStatus需要结合release代码来说明
parkAndCheckInterrupt()
/**
* Convenience method to park and then check if interrupted
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
parkAndCheckInterrupt()就非常简单了, 在前面shouldParkAfterFailedAcquire返回true之后, 阻塞当前线程LockSupport.park(this).
Thread.interrupted()是在LockSupport.park(this)返回之后返回当前线程的中断标记值.
So, 这里的线程获取ReentrantLock.lock在acquiredQueued中阻塞等待的时候有2次p==head&& tryAcquire的竞争锁的机会—这里是一个自旋的过程, 在2次尝试都失败之后, 就进入LockSupport.park线程阻塞阶段. 所以这里的阻塞等待过程既有for(;;) +tryAcquire自旋等待,也有LockSupport.park让出CPU等待。
6. ReentrantLock锁释放
ReentrantLock.unlock调用的是AQS.release方法
public void unlock() {
sync.release(1);
}
AQS.release也是模板方法, 释放锁的逻辑在tryRelease中实现
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
在ReentrantLock.Sync中实现的tryRelease逻辑中,也是对state变量进行操作
protected final boolean tryRelease(int releases) {
int c = getState() -releases;
if (Thread.currentThread()!= getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
首先会检查持有当前锁的线程是否是来释放锁的线程, 如果不是则抛出IllegalMonitorStateException。 在确认是当前线程是持有锁的线程, 则减去state值, 并将ownerExclusiveThread置为null.
在释放完锁之后, 其他等待该锁的线程就可以去竞争这个锁了, 前面讲述了线程在尝试几次获取锁失败之后就会进入阻塞等待(Waiting)状态, 让出CPU. 那么在当前线程将锁释放之后,自然就需要去唤醒这些等待的线程, 让它参与到竞争锁的过程中来。
这里就是release中的unparkSuccessor(h)的内容了:
head == null表示等待队列为空, head.waitStatus == 0 也就是head.waitStatus != Node.SIGNAL, 也就表示head.next节点代表的线程没有被阻塞, 就不需要去唤醒.
unparkSuccessor(h)
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {// 处理线程被取消的情况 s = null;
for (Node t = tail; t != null && t !=node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
1. 先清理head上的标记(waitStatus), 然后在唤醒(head.next)所代表的线程
2. 中间的判断是用来处理等待的线程被取消的情况(s.waitStatus >0 也就是Node.CANCEL)的, 如果出现被取消的情况,就从tail倒序遍历找到没有一个没被取消的线程唤醒.
3. 为什么是从tail找,而不是从head一直遍历下去, 第一是还需要处理null的情况,第二是一般线程被取消也是排在前面的先被取消, 从后向前遍历是尽量快的找到一个未被取消的线程.
release时候清理head.waitStatus +唤醒head.next:LockSupport.unpark(head.next)和前面acquireQueued的时候标记prev.waitStatus=Node.SIGNAL + 阻塞当前线程(node节点记录当前线程)刚好对应上. 至于为什么用prev节点来记录当前线程的阻塞状态还是没有说明好,得画个图来看看。
7. ReentrantLock的可重入特性
在AQS中有个ownerExclusiveThread可以用来记录独占锁持有者是哪个线程, 在持有者线程再次获取该锁的时候,就不用compareAndSetState来获取锁了,直接重入增加锁次数.
不管是fair/non-fair模式, 都是通过ownerExclusiveThread来实现锁的可重入特性的。
8. ReentrantLock可中断特性
在ReentrantLock中提供了lockInterruptibly()方法可以让线程在等待锁的时候响应线程中断请求, 那么在thread.interrupt后是怎么响应的?
ReentrantLock.lockInterruptibly()和lock()方法在内部实现还有点不一样:
public void lockInterruptibly() throws InterruptedException
{
sync.acquireInterruptibly(1);
}
ReentrantLock.lockInterruptibly在内部是使用的AQS.acquireInterruptibly实现的
public final void acquireInterruptibly(int arg)throws InterruptedException
{
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
AQS.acquireInterruptibly在最开始就先判断了一下线程的中断标记, 如果已打中断标记,抛出InterruptedException(这里是第一处ReentrantLock.lockInterruptibly对中断的响应), 但是还没有到线程阻塞的时候是怎么对线程中断响应的.
在AQS.acquireInterruptibly中tryAcquire操作和前面没有响应中断的一样(都是第一次尝试获取锁,是否可以重入等判断)。 在第一次tryAcquire失败之后,就进入doAcquireInterruptibly方法,看方法就知道这里面也对线程中断做了响应。
doAcquireInterruptibly
/**
* Acquires in exclusive interruptible mode.
* @param arg the acquire argument
*/
private void doAcquireInterruptibly(int arg) throws InterruptedException
{
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在doAcquireInterruptibly中和前面acquireQueued(addWaiter(Node.EXCLUSIVE),arg)流程其实是一样的, 只是在对阻塞之后的处里不同, acquireQueued中只是记录Thread.interrupted状态, 而在doAcquireInterruptibly中则抛出了InterruptedException.
在前面acquireQueued中我们知道当前线程该阻塞(shouldParkAfterFailedAcquire ==true)的时候, 在parkAndCheckInterrupt中会使用LockSupport.park(this)阻塞当前线程。
parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() { LockSupport.park(this);
return Thread.interrupted();
}
那在这里阻塞的时候是怎么样响应线程中断的.
Answer: LockSupport.park(this)在阻塞当前线程后, 如果遇到当前线程被中断, LockSupport.park不会抛出InterruptedException,也不会清除线程中断标记,只会默默的返回, 然后执行后续代码。 parkAndCheckInterrupt返回当前线程的中断标记状态(true) , 条件if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())成立, 就会抛出InterruptedException. 这里就完成了线程在阻塞等待锁的时候对线程中断请求的响应.
9. ReentrantLock超时特性
ReentrantLock.tryLock()提供了非阻塞的方式获取锁, 只尝试一次获取锁记录, 如果没有获取到锁返回false ; 获取锁记录成功则返回true. 不会在拿不到锁的时候进入队列等待.
ReentrantLock.tryLock
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
eentrantLock.tryLock使用了Sync的非公平实现,不用入队列,抢占式的获取锁。
ReentrantLock.tryLock同样提供了带timeout版本,tryLock(timeout, unit)实现了在指定等待时间内没有获取到锁,则返回false.
Question:在这里先想想, tryLock(timeout,unit)是怎么样实现超时等待的?
Answer:在这里我想的是, 当前线程tryAcquire没有获取到锁,就LockSupport.parkNanos(timout)指定时间, 其他线程在锁释放的时候唤醒该线程, 竞争不到锁就再次进入LockSupport.parkNanos(remainTimeout). 这里有想到了, 如果有多个线程在超时等待是不是也要排队, 那不是就和前面的ReentrantLock.lock实现一样了, 只是将LockSupport.park换成LockSupport.parkNanos. 这样既能在锁被释放的时候唤醒,线程中断的时候响应,还能在超时时间到达的时候解除阻塞,返回false表示没有获取到锁。那么来看看是不是和我们想的一样,是和ReentrantLock.lock实现一样的。
ReentrantLock.tryLock(timeout, unit)
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
{
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
同前面ReentrantLock方法实现一样,这里也是将功能委托给AQS来实现的.v
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException
{
if (Thread.interrupted())
throw new InterruptedException(); return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
这里是先判断了一下线程中断状态, 决定是否抛出InterruptedException。 看来还是想的有点出入的, 应该是和ReentrantLock.lockInterruptibly类似的实现, 因为tryLock(timeout, unit)是超时实现, 应该响应线程中断。
tryAcquire这个不用将了, 都是去尝试第一次获取锁, 具体实现细节取决于锁是公平还是非公平的。如果是公平的, 第一次都是失败的, 需要入队列之后再次尝试获取。
接下来进入关键了, doAcquireNanos应该就是对超时特性的实现了。
doAcquierNanos
在第一次tryAcquire获取锁失败之后,线程入队列addWaiter和成功获取锁之后的操作都是一样的.
不一样的是对入队列后尝试获取锁失败(p==head && tryAcquire)后的操作. 如果获取锁失败后,时间已经超过timeout->deadline, 那么表示已经超时,返回false. 如果还没有到timeout->deadline, 那么shouldParkAfterFailedAcquire来决定是继续尝试获取锁还是应该等待(这部分逻辑和前面所有的都是一样的) , 如果判定应该等待, 但是剩余的timeout时间比自旋最低时间(spinForTimeoutThreshold)要长,那么就可以等待,也确实是前面想的使用LockSupport.parkNanos来完成超时阻塞等待工作.
如果剩余timeout比自旋最低时间(spinForTimeThreshold)要小, 还是老实进行自旋操作吧。
这里LockSupport.parkNanos会有3种情况不在进行阻塞
1. 阻塞timeout时间到达,自然不在进行阻塞
2. 其他释放锁,唤醒第一个等待的线程,恰好该线程就是的,也不会再进行阻塞
3. 当前parkNanos的线程发生线程中断, parkNanos响应中断时间,直接返回,也不再进行阻塞
前面不管是进入下一次自旋还是parkNanos不在阻塞, 都会判断一下线程中断状态,根据线程中断状态决定是否抛出InterruptedException。 这里实现了对线程中断的响应。
总结,ReentrantLock.tryLock(timeout,unit)通过自旋+LockSupport.parkNanos实现了超时等待,同时在自旋和parkNanos后判断了线程中断状态,实现了对线程中断的响应。在ReentrantLock.tryLock(timeout,unit)中是使用的tryAcquire来尝试获取锁的, fair/non-fair有不同的实现, 也就实现了公平/非公平特性.
所以ReentrantLock.tryLock(timeout,unit)和ReentrantLock.tryLokc()还是有点不一样的.
10. Lock + Condition
内置条件队列存在一些缺陷,每个内置锁都只能有一个相关的条件队列。 相对于内置队列, Lock可以有任意个Condition对象。
和Lock/synchronized类似, Condition也比内置条件队列提供了更丰富的功能: 在每个锁上可能存在多个条件等待; 条件等待可以中断获取不可中断, 基于时限的等待以及公平或非公平的队列操作。
看AQS相关的类图,在AQS中实现Condition接口的是内部类ConditionObject.
在ReentrantLock.newCondition的时候也就是直接返回的一个ConditionObject对象。
ReentrantLock.newCondtion
public Condition newCondition() {
return sync.newCondition();
}
AQS.newCondition
final ConditionObject newCondition() {
return new ConditionObject();
}
(ps: 相比较于synchronized + object.wait/notify内置条件队列, 这里就可以有多个ConditiontObject, 可以构建多个条件队列, 将多个条件谓词分开了)
上面关于条件谓词的相关术语摘自于<JAVA并发编程实战>一文中的’使用条件队列’一节中。
对于Conditon, 提供了如下的访问方法
await() throws InterruptedException
awaitUninterruptibly()
awaitNanos(time, unit) throws InterruptedException
awiatUnti(date) throws InterruptedException
signal()
signalAll()
先来研究一下await() throws InterruptedException
await() throws InterruptedException: 抛出了InterruptedException, 看来是要响应线程中断, 同时在condition.await在阻塞的同时还会释放对应的锁.
在这里又有2个小疑问:
1. condition.await是怎么阻塞线程
2. condition.await是怎么释放对应的锁的和在被唤醒之后又是怎么重新获得锁的?
ConditionObject.await
/**
* Implements interruptible condition
wait.
* <ol>
* <li> If current
thread is interrupted, throw InterruptedException.
* <li> Save lock state returned by {@link #getState}.
* <li> Invoke {@link#release} with saved state as argument,
* throwing IllegalMonitorStateException if it fails.
* <li> Block until signalled or interrupted.
* <li> Reacquire by invoking specialized version of
* {@link #acquire} with saved state as argument.
* <li> If interrupted while blocked in step 4, throw InterruptedException.
* </ol>
*/
public final void await() throws InterruptedException
{
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState =fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node))
{
LockSupport.park(this);
if ((interruptMode =checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState)&& interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up ifcancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
注释中对condition.await实现的行为描述:
1. 如果conditon.await阻塞的线程被中断,则抛出InterruptedException
2. 在释放锁的时候保存锁状态(state)值,通过getState方法返回的
3. 调用release方法释放锁, 释放失败抛出IlegalMonitorStateException
4. condition.await阻塞知道被唤醒或者被中断
5. 在被唤醒之后, 通过指定的acquire重新获取锁
6. 如果在step4阻塞的时候被中断,则跑出InterruptedException
(翻译有待改进, 暂时只是直译)
上面对condition.await中的行为进行了描述, 下面就来看看await中代码是具体怎么实现这些行为的:
1. 在最开始,可以看到先判断了一下condition.await当前线程的中断状态,如果已中断,则抛出InterruptedException.
2. 紧接着就是将当前线程加入到条件等待队列中
Node node = addConditionWaiter();
这里暂时还看不出来有什么作用, 不过条件队列中的Node节点和前面锁等待队列中的节点是一个类.
可以看到条件队列也是采用链表实现的, Node节点类也是和锁等待队列中的一样, 不过条件队列中的waitStatus = Node.CONDITION ; 而且条件等待队列是一个单向链表, 通过nextWaiter来连接的, 锁等待队列是双线链表实现的, prev/next来联接的.
3.在当前线程入条件等待队列后, 就开始释放condition.await线程持有的对应的锁了:fullRelease
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)){
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
这里可以看到先通过getState获取锁状态值, 然后通过release方法将当前锁的状态值全部释放(即saveSatete -> 0, ownerExclusiveThread=null) .
因为这里ReentrantLock中lock是独占锁, 所以这里的全部释放也只是释放当前线程加锁的, (考虑到可重入,所以是full-release).
这里将state -> 0, 其他等待该锁的线程才能获取到锁,因为在ReentrantLock中tryAcquire中是使用compareAndSetState(0,acquires)来尝试加锁的. 只有full-release才能是真正的释放了锁, 其他线程才有机会.
这里release中的tryRelease在ReentrantLock实现中会检查是不是当前线程持有condition对应的锁, 如果没有只有则抛出IllegalMonitorStateException. (出现这种情况最常见的就是, 在condition.await之前没有使用Lock.lock获取锁. 在编写程序的时候可以经常看到)
4. 在condition.await释放对应的锁之后,就是相应的阻塞当前线程的代码了:
这里LockSupport.park(this)就是阻塞当前线程的代码了, 看起来和Lock.lock中阻塞当前线程也是一样的(在JUC中阻塞/释放的实现基本都是LockSupport.park/unpark实现的); 这里LockSupport.park阻塞之后只有被唤醒或者线程中断才会结束阻塞。
Question:这里为什么会多出一个while(!isOnSyncQueue(node) {…} 判断条件队列节点不再锁同步等待队列中,这里是用来做什么的? 在addCondtionWaiter中添加条件队列节点的时候不是本来就不再锁等待队列中么?
Answer: 在这里就要考虑一下多线程并发的情况, condition.await在full-release之后, 其他线程就可以获取到锁了,当其他线程获取到锁执行完业务逻辑之后,一般都是使用的condition.signal/signalAll来唤醒条件等待(condition.await)队列中的线程,在signal/signalAll中会将条件等待队列中的线程(node)移动到锁等待队列中. !isOnSyncQueue(node)处理的就是在full-release之后,当前线程马上被唤醒转移到锁等待队列的情况, 这里当然也就用不到LockSupport.park(this)来阻塞了.
(上面需要结合condition.signal/signalAll的代码来看)
and 这里while(!isOnSyncQueue(node){} 中while又是处理的什么情况,为什么不是if判断? 难道还 有LockSupport.park(this)返回不都是线程被唤醒进入锁等待队列或者线程被中断2种结果么?
Answer:
这里是处理的condition.signal只唤醒一个线程的情况,
+TODO while循环是处理的什么
参考文档
1. condition.await阻塞之后, condition.await的线程执行情况是怎么样的,也就是怎么被唤醒的(在刚开始看condition.signal的时候发现没有LockSupport.unpark已下子整蒙了, 看了这个才想起来有Lock.unlock这回事)
5. 下面就是被唤醒或者中断之后的处理流程了
在被唤醒之后重新获取锁acquireQueued(node, savedState) : 这里的node代表的线程在被唤醒之后node节点就从条件等待队列中移动到锁等待队列中, 然后就和其他线程一起竞争锁。
这里为什么要acquireQueued(node,savedState)要带上savedState去竞争锁, 这是因为ReentrantLock有可重入的情况, 每次释放锁的时候state值会减去1, 如果state=0,还会将ownerExclusiveThread置为null. 如果savedState值对不上,就可能在后面Lock.unlock的时候发现ownerExclusiveThread已经是null了, 就会抛出IllegalMonitorStateException.
这里看起来 LockSupport.park中断退出阻塞也要重新获取锁, 为啥?
Answer: +TODO 中断响应为什么不是直接抛出异常,也要走重新获取锁的流程
ConditionObject.signal/signalAll
在前面condition.await中我们说到await在阻塞(LockSupport.park)的时候, 会有2种方式中断阻塞流程: 1. condition.signal/signalAll唤醒线程 2. Thread.interrupt()中断线程
还提到condition.signal/signalAll在唤醒条件等待队列节点的线程的同时还会将条件等待队列节点转移到锁等待队列中,方便在唤醒之后重新竞争锁。
现在就来看看condition.signal/signalAll是怎么样唤醒的,以及是怎么样将node节点从条件等待队列移动到锁等待队列的。
condition.signal (ConditionObject)
/**
* Moves the longest-waiting thread, if one exists, from the
* wait queue for this condition to the wait queue for the
* owning lock.
*
* @throws IllegalMonitorStateExceptionif {@link #isHeldExclusively}
* returns {@code false}
*/
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
可以看到在使用condition.signal的时候也是要持有锁才可以的,不然也是抛出IlegalMonitorStateException.
在condition.signal中选择唤醒的线程是等待最久的(firstWaiter), 如果firstWaiter==null也就表示没有等待的线程,也就不用唤醒了. 实际进行唤醒操作的还是doSignal操作.
doSignal
/**
* Removes and transfers nodes until hitnon-cancelled one or
* null. Split out from signal in part toencourage compilers
* to inline the case of no waiters.
* @param first (non-null) thefirst node on condition queue
*/
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first)
&& (first = firstWaiter) != null);
}
emm, 实际的唤醒操作也还是在transferForSignal中,其他的代码都是在唤醒一个条件等待队列时对条件等待队列的维护.
if ( (firstWaiter = first.nextWaiter) == null)
+ first.nextWaiter = null;
这2行代码,就是将要唤醒的条件等待队列节点从firstWatier/lastWaiter中维护的这个条件等待队列中移除.
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
这里也是很自然了, (firstWaiter= firt.nextWaiter)== null也就表示没有条件等待队列节点了, lastWaiter也就自然置为null了。
do { } while(!transferForSignal(first) && (first= firstWatier) !=null) 这里do-while的条件?这里是可能tranferForSignal(first)将选择的条件等待队列中的等待最久的节点唤醒并移动到锁等待队列的时候可能失败, 如果失败这里也就不管它代表的线程了, 然后在剩下的条件等待队列中选择等待最久的节点继续唤醒并移动到锁等待队列中, 直到唤醒成功或者条件等待队列中没有要唤醒的节点了. 这就是do-while的作用了, 至于transferForSignal为什么会失败, 我们接下来继续看。
transferForSignal
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
可以看到在最开始通过CAS操作将条件等待队列节点node.waitStatus置为0. 如果这个时候node.status != Node.CONDITION 就表示执行具体唤醒操作失败. 上面注释也说明了, 这种情况就说明是被取消了. 难道是Thread.interrupt()中断将node.waitStatus变化了??这里还没看明白是哪里将node.waitStatus改变了. 先继续下面的内容
+TODO 什么时候node.watiStatus会变成Node.CANCELLED状态, 是不是Interrupt
接下来的 Node p = enq(node); 就是将条件等待队列中的节点加入到锁等待队列中了, 这个node节点在前面的doSignal中已经从条件等待中移除了, 这里只需要将其加入到锁等待队列就可以了。 而且在上一步中, 也已经将node.waitStatus置为0了, 和Lock.lock中锁等待加入到锁等待队列中的节点的waitStatus值保持了一致。 节点入队列enq(node)是返回的node的前置节点。
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
在前面的Lock.lock/unlock中我们也知道了在设计的时候是使用的node.prev节点的waitStatus标识node.thread代表的线程的阻塞状态.
ws >0 : (这里只有Node.CANCELD = 1) 在node从条件等待队列同步到阻塞队列之前, 同步等待队列中的tail(尾节点)被取消.
compareAndSetWaitStatus == false : (什么情况下这里的CAS操作会失败)在node节点同步到阻塞队列之后,在执行CAS之前, tail(尾节点)被取消.
这里2种特殊情况, 所以要提前唤醒node节点代表的线程. (这里为什么特殊,要提前唤醒?)
正常情况下, node节点只需要入队列即可, 然后返回false. 没有LockSupport.unpark唤醒的流程.
Question:那么正常情况下condition.signal没有唤醒的流程, 那node代表的线程在LockSupport.park还是被阻塞着的, 它又这么继续执行阻塞之后的代码了(包括重新获取锁和condition.await之后的业务代码)?
Answer: 这里就要说到condition.signal的使用了, condition.signal/signalAll一般都是在lock.unlock之前调用(为了signal之后尽快的释放锁). 在ReentrantLock.unlock中会释放锁, 重置锁状态state值并选择从等待队列中唤醒一个线程。(在ReentratnLock.unlock中有LockSupport.unpark(node.thread)唤醒的代码)。
这里其实也说明了在condition.signal中的transferForSignal在正常情况下为什么不直接唤醒线程. 因为这里signal唤醒线程,锁还没有被释放。condition.await中被唤醒的线程acquireQueued(node,savedState)去竞争锁的时候锁没被释放也是怎么都竞争不到的。
参考文档:
1. condition.signal中tranferForSignal实际唤醒操作中的enq入队列之后的代码解释
ConditionObject.signalAll
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
前面的逻辑和condition.signal的一样, doSignal换成了doSignalAll
doSignalAll
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
在doSignalAll中的逻辑其实也是和doSignal中的逻辑差不多的, 只不过doSignal只是将一个条件队列节点同步到锁等待队列中; 而doSignalAll就是将condition条件队列中的所有节点都同步到了锁等待队列中. 附上doSignal代码比较一下
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first)
&& (first = firstWaiter) != null);
}
这里在最后也顺便总结一下,condition.await中LockSupport.park(this)会在什么时候停止阻塞
1. LockSupport.park响应线程中断停止阻塞
2. condition.signal后紧跟着的ReentrantLock.unlock中会选择一个节点去唤醒让它进去锁竞争流程
3. 在condition.signal中的transferForSignal中有一个提前唤醒的特殊情况
+TODO
这里最好是画一个ABC循环输出的例子来标识一下条件等待队列和锁等待队列的变化