在复习了ReentrantLock可重入的独占锁后,我们来分析下使用了共享锁的CountDownLatch。
CountDownLatch使用示例
先看一个CountDownLatch的使用示例。
package com.wangcc.springbootexample.concurrency;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(4);
for (int i = 0; i < latch.getCount(); i++) {
new Thread(new MyThread(latch), "player" + i).start();
}
System.out.println("正在等待所有玩家准备好");
latch.await();
System.out.println("开始游戏");
}
private static class MyThread implements Runnable {
private CountDownLatch latch;
public MyThread(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
Random rand = new Random();
int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;// 产生1000到3000之间的随机整数
Thread.sleep(randomNum);
System.out.println(Thread.currentThread().getName() + " 已经准备好了, 所使用的时间为 " + ((double) randomNum / 1000) + "s");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
程序输出如下:
正在等待所有玩家准备好
player2 已经准备好了, 所使用的时间为 1.687s
player1 已经准备好了, 所使用的时间为 1.992s
player0 已经准备好了, 所使用的时间为 2.095s
player3 已经准备好了, 所使用的时间为 2.453s
开始游戏
通过上述示例,我们能够知道CountDownLatch的用处。当一个线程的操作必须要等待其他几个线程执行完后才能继续执行的时候,就可以使用CountDownLatch来实现。在创建CountDownLatch中时,会传递一个int类型参数count,该参数是“锁计数器”的初始状态,表示该“共享锁”最多能被count个线程同时获取。当某线程调用该CountDownLatch对象的await()方法时,该线程会等待“共享锁”可用时,才能获取“共享锁”进而继续运行。而“共享锁”可用的条件,就是“锁计数器”的值为0!而“锁计数器”的初始值为count,每当一个线程调用该CountDownLatch对象的countDown()方法时,才将“锁计数器”-1;通过这种方式,必须有count个线程调用countDown()之后,“锁计数器”才为0,而前面提到的等待线程才能继续运行!
CountDownLatch源码分析
先看构造方法
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
CountDownLatch
与ReentrantLock
一样,有一个内部类,名为Sync,是AQS子类。CountDownLatch#Sync
调用的是AQS对于共享锁的实现。在构造方法中,会将锁计数器的值count通过Sync的构造方法赋给AQS中的state属性。
Sync(int count) {
setState(count);
}
//AQS
protected final void setState(int newState) {
state = newState;
}
接下来看countDown()
方法
public void countDown() {
sync.releaseShared(1);
}
直接调用AQS中的final方法releaseShared
方法,该方法是尝试释放共享锁的方法,在分析AQS中分析过,是否成功释放共享锁,要看子类实现的tryReleaseShared()
方法是否返回true,返回true则代表成功释放锁,就会去调用doReleaseShared()
方法去唤醒阻塞线程。我们看下tryReleaseShared()
的实现。
//CountDownLatch#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;
//CAS将state值置为nextc
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
实现非常简单,当state状态为0的时候,也就是当前共享锁已经释放了,那么就返回false。否则将state-1,CAS设置新的state值,去比较state-1
的值是否等于0,如果等于0,那么就返回true,否则就返回false。通过这个方法,我们知道返回true的时候就是第count个线程调用countDown()
的时候。
最后来看下await()
方法
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
直接调用了AQS中的final方法acquireSharedInterruptibly()
方法。这个方法的具体实现在AQS中已经分析过,我们知道当子类实现的tryAcquireShared()
方法的返回值小于0的时候,会导致当前线程进入自旋去尝试获取锁。否则当返回值不小于0的时候,也就意味着当前线程已经获取到了共享锁。
tryAcquireShared(arg)
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
方法简单到令人发指,当state为0的时候就返回1,否则就返回-1。所以只有当有count个线程调用了countDown()
方法后,才会获取到共享锁继续执行。AQS的设计就是这么好,我们只需要写很少的代码就能实现一个锁。