Lock
JUC中各种锁的主要继承对象之一。
public interface Lock {
/**
获取锁。如果当前获取不到锁,不进行线程调度且休眠直到已获取到锁。
**/
void lock();
/**
获取锁,除非当前线程被Thread.interrupt了。
**/
void lockInterruptibly() throws InterruptedException;
/**
尝试获取锁。如果可以获取就立即返回true且获取到锁,如果获取不到就立即返回false。
**/
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
看过了使用AQS的ReentrantLock,lock接口也很容易理解了,除了最后一个:Condition。
其实这里的Condition就关联到了另外一个接口:Condition。
Lock+Condition就可以构建一个单锁多条件队列的多线程工作环境,这里可以比较成synchronized关键字+Object,在Condition的头上可以看到Doug Lea的实现note:
{@code Condition} factors out the {@code Object} monitor
methods ({@link Object#wait() wait}, {@link Object#notify notify}
and {@link Object#notifyAll notifyAll}) into distinct objects to
give the effect of having multiple wait-sets per object, by
combining them with the use of arbitrary {@link Lock} implementations.
Where a {@code Lock} replaces the use of {@code synchronized} methods
and statements, a {@code Condition} replaces the use of the Object
monitor methods.
简单地说,Condition对比的是Objec类中通过native方法实现的wait和notify、notifyAll,用于构建多组单对象多等待者得等待队列。Condition+Lock的组合可以用于替代Object+Sync。
这里就先不考虑这个Condition了,后面有空再来看看这个Condition,现在看看Lock。
Lock在JUC中有以下实现类:
- ReentrantLock
- ReentrantReadWriteLock
- StampLock
- ConcurrentHashMap中的Segment
而其实根据我们对于AQS的解析,JUC中的Lock接口不出意外应该基本都是通过AQS进行延展的。也就是说,继承了Lock的类,都是某种意义上的锁,在某些条件下不可共享(当然也有读写锁这种东西)。
CountDownLatch
上一期其实我们解析的和ReentrantLcok有关的AQS,都是独占模式的;接下来我们来看一个共享模式的AQS使用案例:CountDownLatch。
这里需要提前复习一下AQS中的state值:这个值,在ReentrantLock中,代表的含义是当前独占线程重入锁的次数。
使用例
CountDownLatch的使用方式也很简单,我们来看看以下的代码示例:
public static CountDownLatch cdl = new CountDownLatch(100);
public static int res = 0;
public static void task(){
res++;
cdl.countDown();
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
new Thread(LockDemo::task).start();
}
cdl.await();
System.out.println(res);
}
-
首先是
new CountDownLatch cdl = new CountDownLatch(num) -
随后在线程任务中,调用这个
cdl.countDown() -
最后,主线程调用await方法,等待所有相关线程执行
- 如果我们不调用
cdl.await()那么就不控制了,也就是说主线程并不会在此上锁,爱去哪去哪 - 如果调用次数一定小于num,那么cdl就会一直await,除非我们指定等待时间
- 如果调用次数大于num,那么cdl只保证num规模下的同步,超过num的部分,不一定是同步的。
- 如果我们不调用
这里可以看到,CDL的使用跟ReentrantLock一样简单方便,封装得很好。
这个值很容易让我们想到AQS中的state,我们按照顺序,来看看CountDownLatch的原理。
构造方法
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
而这里的sync,跟ReentrantLock中一样是AQS的继承类,这个实现是:
Sync(int count) {
setState(count);
}
这里就对上了。
既然这里的num是用来设置为state的,那么想必countDown方法就是这个state减1了。
countdown
随后,我们所作的是多线程任务中,进行CDL.countDown。
public void countDown() {
sync.releaseShared(1);
}
cdl内部的sync实现并没有重写这个方法,因此需要到AQS中看。
上一次通过ReentrantLock看AQS,实际上并没有涉及这个方法,因此这里实际上对于我们来说是一个新的AQS使用路径:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
这里,会先调用tryReleaseShared,这是一个AQS留的钩子方法,cdl的sync重写了:
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;
}
}
下一步的doReleaseShared被执行的时候就是这里tryReleaseShared,CAS将state值变为0成功的时候才会执行。
- 这里就解释了上面我们所说的那个现象:当countDown的次数大于num的时候这里就直接返回了,不做其他操作。
根据我们对CDL的使用,其实我们是很清楚:
- 当new到countDown的时候,CDL中的sync,其实就是一个只有一个节点的AQS。
那么接下来我们看看这个doReleaseShared:
这个方法并没有被CDL中的sync重写,因此看AQS中的。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
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;
}
}
可以看到这里是一个自旋操作。
首先先查看头节点的状况:
-
如果不是空且后面有节点(头节点不是尾节点):
-
如果头节点状态是signal:
-
CAS改为0,失败则继续自旋
-
如果CAS成功,则唤醒后续节点
- 这里的unparkSuccssor是不是很眼熟?前面的RL里提到过啦,这里会让休眠的线程重新起来竞争一次。
-
-
如果头节点状态是0:
-
CAS设置节点值为PROPAGATE
- 失败回去自旋
-
-
-
如果是空或头=尾(队列就一个节点)
-
如果头节点没变,那就退出自旋
- 因为到了这一步,说明上面的unparkSuccssor并没有让节点往前进,这时候再自旋没意义了
-
但其实cdl中并没有增加节点的操作,因此到这一步的head和tail都是null。
await
现在来看看cdl.await:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//final,因此是AQS里的啦
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
这里就得看看这俩了:
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
从上面看来,这里的tryAcquireShared其实就是获取state值了。
在上一步,我们清楚:这个state其实是不可能小于0的,最多就是等于0。
那么当state不等于0,也就是CDL还没有被全部清除的时候,就会进入下面的doAcquireSharedInterruptibly。
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);
}
}
我们清楚:在CDLawait之前,其实sync的队列里是没有节点,头尾都是null的。
那么这里的addWaiter,就会把这个shared节点变成头节点了。
addWaiter在JUC【1】中已经说过了,这里不再赘述,简单点来说就是自旋地将当前线程CAS地塞到AQS队列里,变成最后的节点。
这里在addWaiter之后,当前节点就是最后的节点了,前置节点就是head。
此时回去tryAcquiredShared,跟上面一样,其实就是判断当前CDL是否countDown完了。
-
如果>0,代表已经完了,那么就把当前节点变为头节点,状态改为propaggate,然后返回;
-
如果<0,那么跟RL里一样,就是park了。
- 会在parkAndCheckInterrupt()这里park,直到被上面doReleaseShared中的unparkSuccessor唤醒。
这里也解释了上面CDL次数大于countDown次数时不返回的情况:这里的tryAcquiredShared一直都会返回-1,也就是说当前线程会一直被park。
CDL总结
这样子,整个流程就清晰了:
流程图见:www.processon.com/view/link/6…
总结一下,其实CDL是利用共享条件下的AQS。
类比到RL上,其实很类似于:我们先假设锁重入了N次,然后再一一解锁。只不过这里的线程的解锁,是多个线程到锁上进行解锁。
而等待就是先看看state是否减完,没减完就拿当前线程去初始化AQS。
- 此时,如果当前线程获取锁失败,则挂起等待解锁完毕被唤醒;
- 如果获取锁成功,就将当前线程节点设置为头节点,状态设置为propagate,但由于CDL的性质,其实这里的设置没有太大的意义。