Java并发包同步辅助器CountDownLatch

引言及简介

前面我们介绍了独占锁ReentrantLock实现的一个同步辅助工具CyclicBarrier, 它能够使一组线程互相等待,今天我们介绍另一种同步辅助器CountDownLatch,它其实可以看着是利用共享锁实现的,只不过它没有使用到类似共享锁Semaphore那么复杂的逻辑,所以它的实现没有直接利用Semaphore完成,而是直接在AQS的共享式获取/释放同步资源的基础上实现的一个非常简单的同步辅助工具。

 

根据Java Doc的描述,CountDownLatch可以在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。类比CyclicBarrier的一组线程之间的相互等待,CountDownLatch则是一个或一组线程等待另一个线程或另一组线程,所以CountDownLatch与CyclicBarrier各自等待的线程是不同的。并且CountDownLatch内部维护的计数器是不能重置的循环使用的,而CyclicBarrier的计数器是可以被重置循环使用的。

 

通俗的讲,在用给定的计数初始化CountDownLatch之后,所有执行了await()方法的线程必须要等待其他线程执行相应次数的countDown()之后才会返回,否则将一直阻塞。相当于CountDownLatch维护了一个计数器,执行了await()方法的线程必须要等到计数器清零之后才会返回,而其他线程每执行一次countDown(),计数器就会减1,所以其实执行await()方法的线程可以是多个,而执行countDown()的线程也可以是一个线程的多次执行,因为countDown()方法的执行不存在阻塞等待的情况。

 

使用示例

    假设某个主任务在执行过程中,分别需要满足2个条件才能继续往下执行,这两个条件分别由另外两个线程去满足:

final CountDownLatch latch = new CountDownLatch(2);
		
new Thread(){
	public void run() {
		try {
		   System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
		   Thread.sleep(3000);
		   System.out.println("主线程"+Thread.currentThread().getName()+"达成主线程的条件1");
		   latch.countDown();
		   System.out.println("子线程"+Thread.currentThread().getName()+"继续执行");
	   } catch (InterruptedException e) {
		   e.printStackTrace();
	   }
	};
}.start();
 
new Thread(){
	public void run() {
		try {
			System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
			Thread.sleep(5000);
			System.out.println("子线程"+Thread.currentThread().getName()+"达成主线程的条件2");
			latch.countDown();
			System.out.println("子线程"+Thread.currentThread().getName()+"继续执行");
	   } catch (InterruptedException e) {
		   e.printStackTrace();
	   }
	};
}.start();
 
try {
   System.out.println("等待2个子线程达成主线程需要的2个条件...");
   latch.await();
   System.out.println("2个子线程都达成了主线程需要的条件,主线程继续执行");
} catch (InterruptedException e) {
   e.printStackTrace();
}

   执行结果:

子线程Thread-0正在执行
等待2个子线程达成主线程需要的2个条件...
子线程Thread-1正在执行
主线程Thread-0达成主线程的条件1
子线程Thread-0继续执行
子线程Thread-1达成主线程的条件2
2个子线程都达成了主线程需要的条件,主线程继续执行
子线程Thread-1继续执行

   由上面的示例可以看到,CountDownLatch初始化为2,所以需要执行两次countDown()才会返回,否则就一直等到,示例中就是主线程等待2两个子线程,而两个子线程之间不存在相互影响或等待,子线程Thread-0执行了3秒钟就达成了条件1,然后继续做自己的事情,子线程Thread-1却花了5秒钟才达成条件2,然后主线程立即继续往下执行。两个子线程之间不存在相互等待或影响。

 

源码分析

首先,看看CountDownLatch的类结构


从上图可以看出,CountDownLatch非常简单,没有实现和继承任何接口和父类,直接将操作都代理到继承了AQS的静态内部类Sync上。CountDownLatch对外提供的最主要的方法就await/await(timeout)以及countDown():

public void await() throws InterruptedException {
	sync.acquireSharedInterruptibly(1);
}
//直到超时,被中断,或者成功获取到同步资源才返回
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
	return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
//释放一个同步资源
public void countDown() {
	sync.releaseShared(1);
}

public long getCount() {
	return sync.getCount();
}

   从源码可以发现,CountDownLatch确实利用的AQS的共享锁的逻辑,await必须要获取到一个同步资源才能返回,而countDown则是释放一个同步资源,接着看Sync的逻辑:

private static final class Sync extends AbstractQueuedSynchronizer {
	private static final long serialVersionUID = 4982264981922014374L;

	Sync(int count) { //初始化方法,即初始化CountDownLatch时传入的计数器
		setState(count);
	}

	int getCount() { //获取当前剩余的同步资源个数
		return getState();
	}
        //await方法最终的调用次方法尝试获取同步资源
	protected int tryAcquireShared(int acquires) {
		return (getState() == 0) ? 1 : -1; //只有当剩余同步资源为0时才表示成功。
	}
        //countDown方法最终的调用方法-释放同步资源
	protected boolean tryReleaseShared(int releases) {
		// Decrement count; signal when transition to zero
		for (;;) {
			int c = getState();
			if (c == 0) //已经清0,直接返回false
				return false;
			int nextc = c-1;//直接做减1操作,如果成功,并且减到0才返回true,这样才会唤醒调用await阻塞的线程
			if (compareAndSetState(c, nextc))
				return nextc == 0;
		}
	}
}

   由以上的源码可以很清楚CountDownLatch的实现,其原理大致是,使用CountDownLatch的构造方法传入一个正整形数字的参数之后,由AQS维护这相同数量的同步资源个数,调用await的线程除非在线程被中断,等待超时(如果有超时时间的话),或者该同步资源个数为0的时候才会返回,否则一直等待。而当有线程执行了countDown方法之后,如果当前同步资源个数不为0就减1,每执行一次countDown就减一次1,直到减到0才会唤醒等待的线程。减到0之后,如果继续执行countDown则不会有任何反应。

所以,当await()方法因为其他线程执行了countDown将计数器减至0被唤醒之后,再次调用await()方法时,肯定是会立即返回的,不论有没有执行countDown,因为其计数器一旦被清0,将无法被重新还原,这也就是CountDownLatch不能被重用的原因。

内存可见性

由于CountDownLatch其内部机制其实就是对声明在AQS中的volatile修饰的state变量的维护,所以CountDownLatch也自然满足volatile语义带来的happens-before原则,即“对一个volatile变量的写操作先行发生于后面对这个变量的读操作”, 因此可以得出,在其他线程中调用countDown(写volatile变量)之前的操作happens-before另一个线程执行await(读volatile变量)返回的操作。

也就是说,在其他线程执行countDown之前对共享变量的修改对执行await的线程在await方法返回之后是立即可见的。而那些执行countDown的多个线程(如果存在多个线程的话)之间却不能得出可见性结论。

猜你喜欢

转载自pzh9527.iteye.com/blog/2421845