在并发操作中,当需要当前线程c等待另一线程a结束后在运行的话,我们首先想到的是join方法,在c线程运行中调用a.join(),该方法会使当前线程阻塞于a,直到线程a运行结束,JVM调用a.notifyAll()方法唤醒z。
在Java1.5之后,并发包提供的CountDownLatch也可以实现join功能,并且更为强大。
一、使用方法
下面放出一个简单的Demo:
public static void main(String[]args){
CountDownLatch c = new CountDownLatch(2);
Thread a = new Thread(new Runnable(){
@Override
public void run() {
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("a");
c.countDown();
}
});
Thread b = new Thread(new Runnable(){
@Override
public void run() {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("b");
c.countDown();
}
});
a.start();
b.start();
try {
c.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("main");
}
/*
a
b
main
*/
CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。每当调用CountDownLatch的countDown方法,N就会减1。CountDownLatch的await方法会阻塞当前线程,知道N变为0。由于countDown方法可以用在任何地方,所以既可以是N个线程, 也可以是1个线程N个步骤。在使用多线程时,只需要把这个CountDownLatch的引用传入线程即可。此外,CountDownLatch也允许多个线程调用await方法,同时阻塞多个线程。
还有一点要注意的是,CountDownLatch的计数器无法被重置。
二、实现原理
同读写锁一样,CountDownLatch本质上也是一个共享锁。它允许一个或多个线程等待其他线程。它的实验原理也是同读写锁一样,通过队列同步器(AbstractQueuedSynchronizer AQS)实现的:
构造方法:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
构造器传入的计数count被用来构造一个Sync(AQS)对象。Sync的构造函数如下:
Sync(int count) {
setState(count); //设置同步状态
}
这里,同步器的state就是一个锁状态,当state大于0时,锁被占用,阻塞的线程就不能被唤醒。
阻塞方法:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
这里调用了同步器的共享式获取同步状态,如果当前线程未获取同步,则会进入同步队列等待。并且此方法响应中断。
同步器中该方法源码如下:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException(); //响应中断
if (tryAcquireShared(arg) < 0) //尝试获取共享锁
doAcquireSharedInterruptibly(arg); //尝试失败 线程进入阻塞
}
该方法为模板方法,关键在于重写的tryAcquireShared方法,如下:
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
尝试获取共享锁,当锁计数器==0,即锁是可以获取的,则返回1,获取锁成功,直接退出acquireSharedInterruptibly方法。否则返回-1,进入doAcquireSharedInterruptibly方法阻塞当前线程。doAcquireSharedInterruptibly方法源码如下:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED); //创建当前线程的Node节点,且标记为共享锁,将锁加入CLH队列末尾
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);
}
}
countDown方法
public void countDown() {
sync.releaseShared(1);
}
实际上调用队列同步器的释放锁方法:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
首先会尝试直接释放共享锁,成功则直接返回,否则调用doReleaseShared();
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; //状态-1
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
三、总结
CountDownLatch是通过共享锁实现的。在创建CountDownLatch中时,会传递一个int类型参数count,该参数是队列同步器中的state状态值,表示共享锁最多能被count线程同时获取。当某线程调用该CountDownLatch的await()方法时,该线程会等待共享锁可用时,才能获取共享锁。而共享锁的可用条件就是state等于0。只有每次调用CountDownLatch的countDown方法,状态值才会减1。