Java并发编程之你们常说的CAS到底是个啥?!.
Java并发编程之一张图理解ReentrantLock.
前面的文章理解了java中实现的乐观锁CAS思想,也理解了ReentrantLock中AQS的思想,今天主要介绍JUC中常用的几个工具的实现原理,看完后会发现java中的并发控制工具实现原理大同小异。JUC工具主要介绍以下三种,本文主要讲解ContDownLatch的实现原理。
- ContDownLatch
- Semphore
- CyclicBarrier
如果你还没有使用过相关工具,建议先快速过一遍下面两个博客,了解工具的API使用。
Java并发编程之CountDownLatch.
ava并发编程之Semaphore.
ContDownLatch
首先看图。
1.new CountDownLatch(count)
//CountDownLatch
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
//Sync
Sync(int count) {
setState(count);
}
protected final void setState(int newState) {
state = newState;
}
在初始化对象的构造方法中,对AQS进行了初始化,并将入参count
赋值给AQS中的state
。
2.await() - 1
//CountDownLatch
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//AbstractQueuedSynchronizer
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
首先调用tryAcquireShared
看当前AQS中state
是否为0,也就是其他子线程是否执行完毕,如果state不为0,调用doAcquireSharedInterruptibly(arg);
。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//加入双向链表
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//获取当前节点的上一个节点
final Node p = node.predecessor();
if (p == head) {
//state是否为0
int r = tryAcquireShared(arg);
if (r >= 0) {
//是
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
addWaiter(Node.SHARED);
:将当前线程构建为一个Node节点,用来构建双向链表(当前线程第一个进入AQS),或者用于将当前线程加入到双向链表中。
之后判断当前state
是否为0,也就是是否所有线程都调用了countDown
,假设当前state不为0,进行阻塞parkAndCheckInterrupt()
。接下来看countDown()中是怎样唤醒的
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
3.countDown()
//CountDownLatch
public void countDown() {
sync.releaseShared(1);
}
调用AQS的releaseShared
方法
//AbstractQueuedSynchronizer
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
//CountDownLatch
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
tryReleaseShared
中CAS操作将当前AQS队列中的state-1
,操作成功后,返回state == 0
,如果等于0,说明当前需要执行的所有线程全部countDown
完毕。则执行doReleaseShared();
方法,释放共享锁(多个线程可同时访问)。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
//头结点的等待状态
int ws = h.waitStatus;
//SIGNAL = -1 代表需要被唤醒
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
方法中,首先取到头节点,判断等待状态,是否需要被唤醒,是则调用unparkSuccessor(h);
方法,来唤醒头节点的下一个节点。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//头节点的下一个节点。
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//遍历找到一个waittatus<0的节点,需要被唤醒的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒
LockSupport.unpark(s.thread);
}
找到头节点的下一个节点s
,s
为空或者大于0(当前线程已经被中断)
,则遍历找到一个需要被唤醒的node节点,赋值给s
,最后调用LockSupport.unpark(s.thread);
唤醒。
下面看被唤醒的线程都做了什么操作。
2.await() - 2
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
/************阻塞在这里。************/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
上面讲到,threadA阻塞在上面代码所示位置,当被最后一个调用countDown
的线程唤醒时,会再次进入自旋,这次再调用int r = tryAcquireShared(arg);
,state
状态就是0了,返回1,执行setHeadAndPropagate(node, r);
方法。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
//当前节点的下一个节点。
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
final boolean isShared() {
return nextWaiter == SHARED;
}
在这里,首先将当前节点设置为头节点,然后找当前节点的下一个节点,如果为空或者下一个节点的nextWaiter
是一个共享节点SHARED(多个线程await指向同一个SHARED)
,证明下个线程也需要被唤醒。
(因为CountDownLatch可以同时await多个,需要全部被唤醒)。
调用doReleaseShared
,这个方法和countDown()
中的一样,当前头节点的下个节点。setHead(node);
,因为当前线程节点已经被设置为头节点,那么下个节点,就是头节点的下个节点。
继续回到本文 2.await() - 2中第一个代码片段中的标记点
/************阻塞在这里。************/
如果双向链表中阻塞了多个线程节点,则唤醒过程是一个一个从头节点到尾节点唤醒。
本文结束。
后续会陆续以画图+阅读源码的方式来搞懂JUC中其他工具的实现原理。
敬请期待。