Java concurrent packet synchronization helper CyclicBarrier

Introduction and Introduction

In the previous chapter, we introduced the exclusive synchronization component ReentrantLock. Today, we will not be too busy to introduce other implementation class synchronization components of the Lock interface. Let's first take a look at a synchronization assistant implemented by JDK based on the ReentrantLock synchronization component and the Condition condition waiting mechanism. CyclicBarrier.

 

CyclicBarrier is a synchronization aid that allows a group of threads to wait for each other to reach a common barrier point. The literal meaning of CyclicBarrier can be called a barrier for cyclic (Cyclic) use. The reason why it is a barrier for cyclic (Cyclic) use is that when all threads waiting for each other are released (or after they have passed the barrier) ), it can be reused. In addition, CyclicBarrier can also let this group of threads waiting for each other reach the common barrier (barrier), and let the thread that arrives at the barrier at the latest complete a specially designated task first, after the designated task is completed. , and then let this group of threads continue to execute down (maybe the completion of execution means the end), including the thread that reaches the barrier at the latest will continue to execute its original logic.

 

It may be difficult for us to understand what CyclicBarrier does through such a description. From my understanding, it can be simply described as: using CyclicBarrier can make a group of threads make threads that are already in place before at least one of them is not in place. Trapped into waiting, until all threads are in place, everyone will continue to execute together (if it has been executed, it means the end). The so-called "placement" is equivalent to a barrier or a door. Only when all the participants come to the door will the door be opened for everyone to pass through. After everyone passes, the door will open again. Turn off recovery so that it can be reused for "in place" pending release. As an optional special task, after everyone is seated, in the gap before the door opens, you can let the latest participant to complete a special task temporarily (who asked you to come last? ), after this task is completed, everyone will go through this door together again, and then what to do.


The above picture is the CyclicBarrier I understand. After threads A, B, and C spend different time reaching a common barrier, they continue to execute together (of course, it can end without logic to be executed), when thread B reaches a common barrier. Before, thread C has waited for 5 seconds, and thread A has waited for 3 seconds. If there is a special task specified at the barrier point, before threads A, B, and C continue to execute through the barrier, thread B (because It arrives at the latest) to complete the special task.

 

Example of use

Through the above introduction, I believe that I should have an understanding of the role of CyclicBarrier. Let's use a few examples to deepen our understanding of it. Before using it, we must first understand the construction method of CyclicBarrier, which has two construction methods:

public CyclicBarrier(int parties, Runnable barrierAction) { //Accept a special task barrierAction at the barrier point
	if (parties <= 0) throw new IllegalArgumentException();
	this.parties = parties;
	this.count = parties;
	this.barrierCommand = barrierAction;
}
//Construct a CyclicBarrier directly, which will make the specified number of participants (threads) wait for each other before reaching the barrier point
public CyclicBarrier(int parties) {
	this(parties, null);
}

   Through the construction method, we can find that the special task at the barrier point is optional. The task is an instance that implements the Runnable interface. The integer parameter parties indicates how many threads are allowed to wait for each other to the barrier point.

Example 1 :If several threads need to write data operations, and only after all threads have completed the write data operations, these threads can continue to do the following things

public static void main(String[] args) {  
	int N = 4;  
	CyclicBarrier barrier  = new CyclicBarrier(N, new Runnable(){  
  
		@Override  
		public void run() {  
			System.out.println("Start by thread"+Thread.currentThread().getName()+"Execute special task of barrier point");    
			try {  
				Thread.sleep(2000);  
			} catch (InterruptedException e) {  
				e.printStackTrace ();  
			}   
		}  
		  
	});  
	for(int i=0;i<N;i++)  
		new Writer(barrier).start();  
}  
static class Writer extends Thread{  
	private CyclicBarrier cyclicBarrier;  
	public Writer(CyclicBarrier cyclicBarrier) {  
		this.cyclicBarrier = cyclicBarrier;  
	}  
  
	@Override  
	public void run() {  
		System.out.println("Thread"+Thread.currentThread().getName()+"Writing data...");  
		try {  
			Thread.sleep((long)(Math.random() * 10000)); //Sleep to simulate write data operation  
			System.out.println("Thread"+Thread.currentThread().getName()+"Complete writing data, wait for other threads to finish writing");  
			int index = cyclicBarrier.await();  
			System.out.println("All threads are written, the current thread"+Thread.currentThread().getName()+"(await return: "+index+") continue to process other tasks...");  
		} catch (InterruptedException e) {  
			e.printStackTrace ();  
		}catch(BrokenBarrierException e){  
			e.printStackTrace ();  
		}  
	}  
}

    Results of the:

Thread Thread-2 is writing data...
Thread Thread-1 is writing data...
Thread Thread-3 is writing data...
Thread Thread-0 is writing data...
Thread Thread-0 finishes writing data and waits for other threads to finish writing
Thread Thread-2 finishes writing data and waits for other threads to finish writing
Thread Thread-3 finishes writing data and waits for other threads to finish writing
Thread Thread-1 finishes writing data and waits for other threads to finish writing
The barrier point special task is started by thread Thread-1
After all threads are written, the current thread Thread-1 (await return: 0) continues to process other tasks...
After all threads are written, the current thread Thread-0 (await return: 3) continues to process other tasks...
After all threads are written, the current thread Thread-2 (await return: 2) continues to process other tasks...
After all threads are written, the current thread Thread-3 (await return: 1) continues to process other tasks...

    As can be seen from example 1, we have constructed four threads that wait for each other. Before thread Thread-1 has finished writing data, the other three fastest threads have been waiting. When all four threads reach the barrier point, let The thread Thread-1 that is executed at the latest executes the special task of the barrier point. After the special task is executed, the four threads continue to execute together. And the return value of the await() of the thread Thread-0 that reached the earliest is 3, and the return value of the await() of the thread Thread-1 that reached the barrier point at the latest is 0.

Example 2 :Continue to transform Example 1 to demonstrate the reusability of CyclicBarrier.

public static void main(String[] args) {
	int N = 4;
	CyclicBarrier barrier  = new CyclicBarrier(N,new Runnable() {
		@Override
		public void run() {
			System.out.println("Start by thread"+Thread.currentThread().getName()+"Execute special task of barrier point");
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace ();
			}
		}
	});
	 
	for(int i=0;i<N;i++)
		new Writer(barrier).start();
}
static class Writer extends Thread{
	private CyclicBarrier cyclicBarrier;
	public Writer(CyclicBarrier cyclicBarrier) {
		this.cyclicBarrier = cyclicBarrier;
	}

	@Override
	public void run() {
		int count = 2;
		try {
			while (count > 0) {
				count--;
				System.out.println("线程"+Thread.currentThread().getName()+"第"+(2-count)+"次正在写入数据...");
				Thread.sleep(5000);      //以睡眠来模拟写入数据操作
				System.out.println("线程"+Thread.currentThread().getName()+"第"+(2-count)+"写入数据完毕,等待其他线程写入完毕");
				cyclicBarrier.await();
			}
			System.out.println("所有线程写入完毕,当前线程"+Thread.currentThread().getName()+"继续处理其他任务...");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}catch(BrokenBarrierException e){
			e.printStackTrace();
		}
	}
}

   执行结果:

线程Thread-0第1次正在写入数据...
线程Thread-2第1次正在写入数据...
线程Thread-3第1次正在写入数据...
线程Thread-1第1次正在写入数据...
线程Thread-3第1写入数据完毕,等待其他线程写入完毕
线程Thread-1第1写入数据完毕,等待其他线程写入完毕
线程Thread-0第1写入数据完毕,等待其他线程写入完毕
线程Thread-2第1写入数据完毕,等待其他线程写入完毕
由线程Thread-2开始执行屏障点特殊任务
线程Thread-2第2次正在写入数据...
线程Thread-0第2次正在写入数据...
线程Thread-1第2次正在写入数据...
线程Thread-3第2次正在写入数据...
线程Thread-3第2写入数据完毕,等待其他线程写入完毕
线程Thread-2第2写入数据完毕,等待其他线程写入完毕
线程Thread-1第2写入数据完毕,等待其他线程写入完毕
线程Thread-0第2写入数据完毕,等待其他线程写入完毕
由线程Thread-0开始执行屏障点特殊任务
所有线程写入完毕,当前线程Thread-0继续处理其他任务...
所有线程写入完毕,当前线程Thread-3继续处理其他任务...
所有线程写入完毕,当前线程Thread-2继续处理其他任务...
所有线程写入完毕,当前线程Thread-1继续处理其他任务...

    在示例二中,我让这四个线程循环执行两次模拟数据写入操作,所以他们会遇到两次CyclicBarrier设定的屏障,再他们第一次通过屏障之后,屏障将会被复原,所以第二次遇到屏障的时候,依然会产生和第一次相同的效果,当然这四个线程具体的执行顺序几乎不会和第一次相同了,执行屏障点的特殊任务的线程还是由最晚达到屏障点的线程去完成,第一次是线程Thread-2,第二次是线程Thread-0.

 

通过以上的两个示例我们只能基本的了解CyclicBarrier循环屏障的简单使用,至于其深层次的其他特性,比如:万一存在线程在未到达屏障之前出现了未被捕获的异常,或者线程在执行屏障点的特殊任务的时候抛出了异常,那么又会对其他线程的执行有什么影响呢?这些问题我们稍后再来回答,虽然我们可以通过继续举例来得到验证,但是我还是想先看看CyclicBarrier的源码之后再进行说明,毕竟举例不可能把所有的情况都包含,通过源码分析才能得到最直接最有力的结论。

 

源码分析

首先我们看看CyclicBarrier的成员属性:

public class CyclicBarrier {
	private static class Generation {
		boolean broken = false;
	}
        //锁对象
	private final ReentrantLock lock = new ReentrantLock();
	private final Condition trip = lock.newCondition();
	/** 参与者个数,构造方法参数之一 */
	private final int parties;
	/* 屏障点特殊任务,构造方法参数之一 */
	private final Runnable barrierCommand;
	//用于表示屏障状态
	private Generation generation = new Generation();
        //表示还未达到屏障点的线程个数,为0表示所有线程都达到了屏障点
	private int count;
	....
}
    Through the above analysis of member attributes, it can be found that CyclicBarrier is indeed implemented based on the ReentrantLock synchronization component and the Condition condition waiting mechanism. In addition, CyclicBarrier also has a private static inner class Generation, which only has a Boolean broken attribute for Indicates whether the current barrier is broken. The barrier is not broken by default or after all participants have passed the barrier. Generation is actually the key to realizing that CyclicBarrier can be reused.

Then analyze the core method of CyclicBarrier. Through the above example, you can find that the core relationship method of CyclicBarrier is the await() method, so we can just look at this method directly. Of course, CyclicBarrier also provides an await() with a timeout period. )method.

public int await() throws InterruptedException, BrokenBarrierException {
	try {
		return dowait(false, 0L);//The first parameter is false. The timeout time is 0, indicating that there is no timeout setting
	} catch (TimeoutException toe) {
		throw new Error(toe); // Since the time parameter of dowait() is 0, TimeoutException will never be thrown here.
	}
}
// await() method with timeout
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException {
	return dowait(true, unit.toNanos(timeout)); //Because the time parameter of dowait() is passed in by the user, a TimeoutException may be thrown}      
    It can be seen from the surface that await() can throw interrupt exceptions and BrokenBarrierException exceptions, and even the await(long, TimeUnit) method may throw TimeoutException timeout exceptions. They are all called dowait() methods, and their return value is An integer value returned by the dowait() method. According to the description of the Java Doc, the return value of the await()/dowait() method represents the sequence number of the current thread reaching the barrier point. Assuming that there are 5 threads, the first to reach the barrier point The thread's return value of this method is 4, and then it decreases in turn. The last thread that reaches the barrier point calls this method and the return value is 0. Next, we focus on the dowait() method.
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
	final ReentrantLock lock = this.lock;
	lock.lock(); //Locked by ReentrantLock exclusive lock, unlocked in finally statement
	try {
		final Generation g = generation;
                //If the current barrier has been damaged, directly throw a BrokenBarrierException
		if (g.broken)
			throw new BrokenBarrierException();
                //If the current thread is set with an interrupt flag, first mark the current barrier as "broken", and then wake up all waiting threads
		if (Thread.interrupted()) { //The interrupted state will be reset and cleared
			breakBarrier();
			throw new InterruptedException();//There is also an interrupted exception thrown
		}
                //如果一切正常,计数器减1(因为已经上锁所以没有线程安全问题)
		int index = --count;
		if (index == 0) {  //计数器==0,表示所有线程都已经“就位”
			boolean ranAction = false;
			try {
				final Runnable command = barrierCommand;
				if (command != null)  //如果有特殊任务,执行其run方法
					command.run();//这里如果出现异常也就直接以异常形式返回了
				ranAction = true;
				nextGeneration(); //如果没有特殊任务或者特殊任务执行成功,先唤醒所有等待线程,再重置屏障状态
				return 0;//返回 0
			} finally {
				if (!ranAction) //如果有特殊任务并执行失败,先标记屏障“已损坏”,再唤醒所有等待线程
					breakBarrier();
			}
		}
                //走到这里,说明一切正常,并且还有线程没有“就位”
		for (;;) { // 注意这里是一个"自旋"
			try {
                                //根据调用者是否有指定超时时间,分别调用不同的await()方法,使当前线程陷入阻塞等待,并会释放锁
                                //直到被唤醒、被中断、或者带有超时时间的超时,才会退出等待
				if (!timed)
					trip.await();
				else if (nanos > 0L)
					nanos = trip.awaitNanos(nanos);
			} catch (InterruptedException ie) {
                               //如果是被中断唤醒
                               //检测最新的屏障状态,如果屏障依然完好,就损坏屏障再唤醒其他等待线程,还有抛出中断异常。
				if (g == generation && ! g.broken) {
					breakBarrier();
					throw ie;//这里直接以异常形式返回了
				} else {
                               //如果发现最新的屏障状态为“已损坏”,进行自我中断
					Thread.currentThread().interrupt();
				}
			}
                        //走到这里,说明是被其他线程唤醒,或者超时自动唤醒,或者中断唤醒但是发现屏障已经被损坏
			if (g.broken) //发现屏障损坏立即抛出BrokenBarrierException异常
				throw new BrokenBarrierException();

			if (g != generation)//发现屏障被更新了,返回index
				return index;

			if (timed && nanos <= 0L) { //如果是带有超时时间的等待被唤醒,发现已经没有时间剩下了
				breakBarrier(); //先损坏屏障,再唤醒所有等待线程
				throw new TimeoutException(); //抛出超时异常
			}
		}
	} finally {
		lock.unlock();
	}
}

//先设置屏障状态为“损坏”,再唤醒所有已经在等待的线程
private void breakBarrier() {
	generation.broken = true;
	count = parties;
	trip.signalAll();
}

//Wake up all waiting threads first, and then generate a new available barrier
private void nextGeneration() {
	trip.signalAll();
	count = parties;
	generation = new Generation();
}
   Through the above source code analysis of dowait(), we can roughly sort out its logic as follows:
  1. After entering the await() method, if it is found that the barrier has been destroyed, a BrokenBarrierException is thrown and ended.
  2. After entering the await() method, if the barrier is not destroyed but the current thread is interrupted, the barrier will be destroyed first, then all threads will be woken up, and an interrupt exception will be thrown at the end.
  3. If it is not the last thread to reach the barrier point, the current thread is blocked by the await/awaitNanos method of the Condition object. The index records the sequence number of the current thread reaching the barrier point. The sequence number reached first is the total number of threads minus 1, and the subsequent threads are minus 1 in turn.
  4. If it is the last thread to reach the barrier point, do different things depending on whether the special task of the barrier point was specified in advance,
  5. If there is a specified barrier point task, the barrier point task is executed first, and different logic is executed according to whether an exception is thrown when the barrier point task is executed.
  6. If an exception is thrown while executing a task at the barrier point, the barrier is destroyed before all other threads waiting at the barrier point are woken up. Then throw the exception and end
  7. If no exception is thrown while executing the task at the barrier point, wake up all other threads waiting at the barrier point first, and then regenerate a new barrier. The current thread returns 0 and ends.
  8. If the barrier point task is not specified, then the same logic as after the barrier point task is executed without throwing an exception: first wake up all other threads waiting at the barrier point, and then regenerate a new barrier. The current thread returns 0 and ends.

    The logic after other threads are woken up is as follows:

  1. If it is awakened by an interrupt, and the barrier is not destroyed, the barrier is destroyed first, then wake up other threads, throw an interrupt exception, and end.
  2. If it is woken up by an interrupt and the barrier has been broken, first re-mark the interrupt state, then throw a BrokenBarrierException and end.
  3. If it is woken up normally by another thread and the barrier has been broken, a BrokenBarrierException is thrown immediately and ends.
  4. If it is awakened normally by other threads, and the barrier is not destroyed, but the barrier is updated, return the sequence number index of the barrier point recorded before, and end.
  5. If it is automatically woken up by a timeout and the barrier has been broken, a BrokenBarrierException is thrown immediately, and the end.
  6. If it is automatically woken up by a timeout, and the barrier is not destroyed, but the barrier is updated, return the sequence number index of the barrier point recorded before, and end.
  7. If it is automatically woken up by a timeout, and the barrier is not destroyed and the barrier is not updated, first destroy the barrier to wake up other threads, throw a TimeoutException, and end.
  8. Awakened by other non-interrupting conditions, and the barrier is not destroyed and the barrier is not updated, if a timeout is set but the timeout is not reached, continue to block by spinning.
Through dowait(), it can also be found that the handling of BrokenBarrierException is prior to interruption exception, that is to say, when both interruption and barrier damage occur, BrokenBarrierException must be thrown instead of interruption exception, and interruption will only be interrupted by Relabel. So in addition to the above logic because ① the barrier point task throws an exception, ② the thread is interrupted and awakened, and ③ the thread is awakened by a timeout, in these three cases, the barrier will be automatically destroyed, is there any way for other threads to destroy the barrier by other means? Woolen cloth? The answer is: yes. That is the rest() method provided by CyclicBarrier:
public void reset() {
	final ReentrantLock lock = this.lock;
	lock.lock();
	try {
		breakBarrier(); // Break the barrier first, then wake up all waiting threads
		nextGeneration(); // Wake up all waiting threads before generating a new barrier
	} finally {
		lock.unlock();
	}
}
    Because the rest() method is a public method, any thread can destroy the barrier externally after getting an instance of CyclicBarrier, wake up all waiting threads, and then generate a new barrier. Although the operations of destroying the barrier and generating a new barrier in the rest() method are in a block structure of a synchronization lock, because the memory address pointed to by the generation attribute of the barrier state is directly modified when a new barrier is generated, the old generation The state represented by the memory address pointed to is modified to a "corrupted" state, so for those threads that are already in a waiting state after being awakened by rest(), since they have stored the generation as a temporary variable on the method stack before blocking, so they It will see broken barriers instead of new ones, so they will throw a BrokenBarrierException.

memory visibility

Through the source code analysis of CyclicBarrier, the await method (excluding the thread that finally reaches the barrier) mainly operates in the mode of: lock->condition.await->unlock. The thread that reaches the barrier first falls into condition.await after lock, and only the last The execution logic of an arriving thread is: lock->barrier point special task->wake up all waiting threads->unlock. After the last arriving thread wakes up all other threads blocked by condition.await, those awakened threads need to re-acquire the lock in turn before executing unock. Among them, the lock and unlock operations are actually read and modify operations on the state variable declared in AQS with volatile modification. According to the locking rule of happens-before, "an unlock operation occurs first before the lock operation for the same lock", and The volatile variable rule "writes to a volatile variable occur before reads of this variable" and transitivity lead to the following memory visibility:

 

所有线程在执行CyclicBarrier的await方法之前的操作happens-before屏障点特殊任务中的操作,屏障点特殊任务中的操作又happens-before所有那些从CyclicBarrier的await方法返回之后的操作。也就是说,所有线程在执行CyclicBarrier的await方法之前对共享变量的修改对执行屏障点特殊任务时是立即可见的,而在屏障点特殊任务中对共享变量的修改也对那些线程从CyclicBarrier的await方法返回之后的操作立即可见的。当然,所有线程在执行CyclicBarrier的await方法之前对共享变量的修改对所有线程在从CyclicBarrier的await方法返回之后的操作也是立即可见的。

 

CyclicBarrier运行情况总结

通过对源码的分析,很多未知的疑团终于有了答案, 现将问答一一列举,在使用的时候了解了这些细节才能放心使用。

发生的情况(其它没有指明的情况默认没有发生) 对await()方法产生的结果

最后一个线程到达屏障,并成功执行屏障点任务(

如果有的话)

所有线程返回到达屏障点的序列号,继续往下执行。
最后一个线程到达屏障后,执行屏障点任务抛出异常

该线程将屏障点任务的异常从await()方法抛出;

其他线程立即抛出BrokenBarrierException异常。

某个线程在即将进入await()方法之前被中断

该线程在执行await()方法时立即抛出中断异常;

排在后面执行await()的线程在执行await()时和已经处于等待中的线程将立即抛出BrokenBarrierException异常。

中断某个处于等待中的某个线程

被中断的线程立即抛出InterruptedException异常;

排在后面执行await()的线程在执行await()时和已经处于等待中的线程将立即抛出BrokenBarrierException异常。

某个等待线程超时,并且其他线程都处于等待 该线程直接返回到达屏障点的序列号,其他线程继续等待
某个等待线程超时,并且还有线程未到达屏障

该线程抛出TimeoutException异常,

排在后面执行await()的线程在执行await()时和已经处于等待中的线程将立即抛出BrokenBarrierException异常。

某个线程执行了负值的await()方法

该线程抛出TimeoutException异常,

排在后面执行await()的线程在执行await()时和已经处于等待中的线程将立即抛出BrokenBarrierException异常。

rest()方法被执行

在调用rest()之前已经处于等待状态的线程将立即抛出BrokenBarrierException异常;

而在调用rest()方法之后调用await()的线程将陷入永久的等待,除非它们中至少存在一个带有超时时间。

某个线程在进入await()方法之前发生了异常,导致await()没有被执行。 那些已经处于等待状态的线程和排在后面执行了await()的线程都将陷入永久的等待,除非它们中至少存在一个带有超时时间。

 

Guess you like

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