前面了解了Java并发编程之同步队列,今天研究独占式同步状态的获取与释放的源码分析。
本博客来自《Java并发编程的艺术》的读后感
同步状态的获取主要由模板方法
acquire(int arg)
实现。该方法如下:
acquire()
的官方解释:
Acquires in exclusive mode, ignoring interrupts. Implemented by invoking at least once tryAcquire, returning on success. Otherwise the thread is queued, possibly repeatedly blocking and unblocking, invoking tryAcquire until success. This method can be used to implement method Lock.lock.
Params:
arg – the acquire argument. This value is conveyed to tryAcquire but is otherwise uninterpreted and can represent anything you like.
这个方法是一个独占式获取同步状态的方法,并且不响应中断(即同步队列中的节点不会响应中断而被移出队列)。该方法可以作为Lock接口的lock
方法的实现。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
如上代码所示,
acquire
方法由很简单的几部分组成,结合官方解释以及上一篇文章Java并发编程之同步队列以及各个方法名的字面意义能大概知道实现原理。一个if条件逻辑,如果为true则执行当前线程的自我阻塞。条件判断先判断独占式获取同步状态是否成功(即tryAcquire()
)。获取失败则构建节点并将节点添加到同步队列的队尾(即addWaiter()
)。最后以“死循环”的方式获取同步状态。
获取同步状态的方法需要同步器的实现类去重写,因此这里不阐述
tryAcquire()
方法。同步器的addWaiter()
以及enq()
,方法的代码如下:
addWaiter()
的官方解释:
Creates and enqueues node for current thread and given mode.
Params:
mode – Node.EXCLUSIVE for exclusive, Node.SHARED for shared
Returns:
the new node
将当前线程以及构造出来的节点入队
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;
}
enq()
的官方解释:
Inserts node into queue, initializing if necessary. See picture above.
Params:
node – the node to insert
Returns:
node’s predecessor
如果有必要则初始化节点,并将节点插入到队列
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;
}
}
}
}
解释:
addWaiter()
以及enq()
方法都是使用compareAndSetTail()
方法确保节点能够被线程安全添加。为什么不用LinkedList维护节点之间的关系?只有一个线程获取同步状态成功,其他失败的线程会被放入LinkedList,LinkedList不能保存被放入的节点数量是正确的并且顺序是正确的。如果同步队列是第一次被放进节点,那么将会执行enq()
方法进行初始化队列并死循环地安全放入节点。节点成功放入队尾后,当前线程才能返回。enq()
将并发添加节点的请求通过CAS变得“串行化”。
acquireQueued()
官方解释:
Acquires in exclusive uninterruptible mode for thread already in queue. Used by condition wait methods as well as acquire.
Params:
node – the node
arg – the acquire argument
Returns:
true if interrupted while waiting
队列中的线程(每个节点中都有被阻塞的线程的引用)独占式获取同步状态。该方法被用于condition
等待方法以及acquire()
方法
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);
}
}
如上代码所示,死循环尝试获取同步状态,而只有前驱节点是头节点才能尝试获取同步状态,这是为什么?有2个原因:
- 头节点是成功获取到同步状态的节点。当头节点的线程释放了同步状态后,将会唤醒后继节点。后继节点的线程被唤醒后,会检查它的前驱节点是不是头节点。
- 维护队列的FIFO原则。如下图所示,流程是这样的:非头节点的线程出队或被中断从而从等待状态返回,随后检查自己的前驱是不是头节点,如果是则尝试获取。节点与节点之间基本互不通信,只是简单地判断自己的前驱是否为头节点。这样使得节点的释放规则符合FIFO。(这段不太理解,后期再慢慢考究)
独占式获取同步状态的源码大致是由acquire()模板方法实现。acquire()模板方法由几部分组成:尝试获取同步状态;获取失败则构建成独占式节点并以死循环的方式基于CAS的方法安全地将节点加入到同步队列队尾中;同步队列中的节点进行自旋,当节点的前驱是头节点才尝试获取同步状态;获取同步状态失败则会阻塞那个失败的线程
release()
方法官方解释:
Releases in exclusive mode. Implemented by unblocking one or more threads if tryRelease returns true. This method can be used to implement method Lock.unlock.
Params:
arg – the release argument. This value is conveyed to tryRelease but is otherwise uninterpreted and can represent anything you like.
Returns:
the value returned from tryRelease
独占式释放。可被多线程非阻塞实现,释放成功返回true。该方法可用于实现Lock
接口的unlock()
方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
如上代码所示,释放锁会唤醒头节点的后继节点,
unparkSuccessor(Node node)
方法使用LockSupport
(日后会研究)来唤醒处于等待状态的线程。
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程会被加入到同步队列并不断自旋获取同步状态。被移出同步队列的条件是前驱节点为头节点并且成功获取了同步状态。在释放同步状态时,同步器调用释放锁的方法释放同步状态,然后唤醒头节点的后继节点。