Java concurrent packet synchronization helper CountDownLatch

Introduction and Introduction

Earlier we introduced CyclicBarrier, a synchronization aid implemented by the exclusive lock ReentrantLock, which enables a group of threads to wait for each other. Today we introduce another synchronization aid, CountDownLatch, which can actually be seen to be implemented using shared locks, but it It does not use complex logic like the shared lock Semaphore, so its implementation is not done directly with Semaphore, but a very simple synchronization aid implemented directly on the basis of AQS's shared acquisition/release of synchronization resources.

 

According to the Java Doc, CountDownLatch allows one or more threads to wait until a set of operations that are being performed in other threads are completed. Analogous to the mutual waiting between a group of threads of CyclicBarrier, CountDownLatch is one or a group of threads waiting for another thread or another group of threads, so the threads waiting for CountDownLatch and CyclicBarrier are different. And the counter maintained inside CountDownLatch cannot be reset and used cyclically, while the counter of CyclicBarrier can be reset and used cyclically.

 

In layman's terms, after initializing CountDownLatch with a given count, all threads that execute the await() method must wait for other threads to execute countDown() for the corresponding number of times before returning, otherwise they will be blocked forever. Equivalent to CountDownLatch maintaining a counter, the thread that executes the await() method must wait until the counter is cleared before returning, and every time other threads execute countDown(), the counter will be decremented by 1, so the await() method is actually executed The number of threads can be multiple, and the thread that executes countDown() can also be executed multiple times by one thread, because the execution of the countDown() method does not block waiting.

 

Example of use

    Suppose that a main task needs to meet two conditions in the execution process to continue to execute, and these two conditions are satisfied by the other two threads:

final CountDownLatch latch = new CountDownLatch(2);
		
new Thread(){
	public void run() {
		try {
		   System.out.println("child thread"+Thread.currentThread().getName()+"executing");
		   Thread.sleep(3000);
		   System.out.println("Main thread"+Thread.currentThread().getName()+"Condition 1 of main thread reached");
		   latch.countDown();
		   System.out.println("Sub thread"+Thread.currentThread().getName()+"Continue to execute");
	   } catch (InterruptedException e) {
		   e.printStackTrace ();
	   }
	};
}.start();
 
new Thread(){
	public void run() {
		try {
			System.out.println("child thread"+Thread.currentThread().getName()+"executing");
			Thread.sleep(5000);
			System.out.println("Sub thread"+Thread.currentThread().getName()+"Condition 2 of the main thread reached");
			latch.countDown();
			System.out.println("Sub thread"+Thread.currentThread().getName()+"Continue to execute");
	   } catch (InterruptedException e) {
		   e.printStackTrace ();
	   }
	};
}.start();
 
try {
   System.out.println("Wait for 2 child threads to reach the 2 conditions required by the main thread...");
   latch.await();
   System.out.println("The two sub-threads have reached the conditions required by the main thread, and the main thread continues to execute");
} catch (InterruptedException e) {
   e.printStackTrace ();
}

   Results of the:

Child thread Thread-0 is executing
Waiting for 2 child threads to reach the 2 conditions required by the main thread...
Child thread Thread-1 is executing
The main thread Thread-0 achieves the condition 1 of the main thread
The child thread Thread-0 continues to execute
The child thread Thread-1 achieves the condition 2 of the main thread
The two child threads have reached the conditions required by the main thread, and the main thread continues to execute
The child thread Thread-1 continues to execute

   As can be seen from the above example, CountDownLatch is initialized to 2, so countDown() needs to be executed twice before returning, otherwise it will wait until, in the example, the main thread waits for 2 two sub-threads, and there is no interaction between the two sub-threads. Affect or wait, the child thread Thread-0 executes for 3 seconds to reach condition 1, and then continues to do its own thing, but the child thread Thread-1 takes 5 seconds to reach condition 2, and then the main thread immediately continues down implement. There is no mutual waiting or influence between the two child threads.

 

Source code analysis

First, look at the class structure of CountDownLatch


As can be seen from the above figure, CountDownLatch is very simple. It does not implement and inherit any interfaces and parent classes, and directly delegate operations to the static inner class Sync that inherits AQS. The main methods provided by CountDownLatch are await/await(timeout) and countDown():

public void await() throws InterruptedException {
	sync.acquireSharedInterruptibly(1);
}
//It will not return until it times out, is interrupted, or successfully acquires the synchronization resource
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的多个线程(如果存在多个线程的话)之间却不能得出可见性结论。

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326011884&siteId=291194637