当前位置: 首页 > 科技观察

Java高并发编程基础三大武器TheCyclicBarrier

时间:2023-03-15 13:59:31 科技观察

介绍在上一篇文章中,我们《Java高并发编程基础三大利器之CountDownLatch》有一个缺点,就是它的计数器只能使用一次,也就是说,当counter(state)减为0,如果有其他线程调用await()方法,则该线程直接通过,不再起到等待其他线程执行结果的作用,实现同步。为了解决这个问题,CyclicBarrier应运而生。什么是循环屏障?什么是循环屏障?拆开翻译成Cycle和Barrier。它的主要功能其实类似于CountDownLanch。就是在一组线程到达屏障时阻塞,直到最后一个线程到达屏障,barrier会被打开,所有被barrier阻塞的线程都会继续执行,但是它可以循环执行,这是它和CountDownLanch最大的区别。CountDownLanch的意思是只有当最后一个线程将计数器设置为0时,其他被阻塞的线程才会继续执行。在学习CyclicBarrier之前,建议先阅读这些文章:《Java高并发编程基础之AQS》《Java高并发编程基础三大利器之Semaphore》《Java高并发编程基础三大利器之CountDownLatch》如何使用先来看一个使用CyclicBarrier的demo:比如游戏中有关卡,每次进入都需要使用下一关载入一些地图、特效、背景音乐等,全部载入后才能玩游戏:/**demo源https://blog.csdn.net/lstcui/article/details/107389371*公众号【java金融】*/publicclassCyclicBarrierExample{staticclassPreTaskThreadimplementsRunnable{privateStringtask;privateCyclicBarriercyclicBarrier;publicPreTaskThread(Stringtask,CyclicBarriercyclicBarrier){this.task=task;this.cyclicBarrier=cyclicBarrier;}@Overridepublicvoidrun(){for(intiRandom=0;();try{Thread.sleep(random.nextInt(1000));System.out.println(String.format("任务%soflevel%d已完成",i,task));cyclicBarrier.await();}catch(InterruptedException|BrokenBarrierExceptione){e.printStackTrace();}}}publicstaticvoidmain(String[]args){CyclicBarriercyclicBarrier=newCyclicBarrier(3,()->{System.out.println("Allpre-tasksinthis关卡完成,开始游戏...");});newThread(newPreTaskThread("Loadmapdata",cyclicBarrier)).start();newThread(newPreTaskThread("加载角色模型",cyclicBarrier)).start();newThread(newPreTaskThread("Loadingbackgroundmusic",cyclicBarrier)).start();}}}输出结果如下:我们可以看到每次游戏开始游戏都会等到当前关卡加载完游戏的后才会开始人物模型、地图数据、背景音乐,依然可以循环控制。源代码分析结构();lock:用于保护屏障进入的锁trip:到达屏障无法释放的线程在等待trip条件变量parties:屏障打开所需的到达线程总数barrierCommand:在最后一个线程之后执行到达barrier回调任务生成:这是一个内部类,通过它实现CyclicBarrier复用。每当await达到最大次数时,就会创建一个新的,表示进入了下一个循环。boolean属性只有一个,用来表示当前循环是否有线程中断。主要方法publicintawait()throwsInterruptedException,BrokenBarrierException{try{returndowait(false,0L);}catch(TimeoutExceptiontoe){thrownewError(toe);//cannothappen}}*Mainbarriercode,coveringthevariouspolicies.*/privateintdowait(booleantimed,longnanos)throwsInterruptedException,BrokenBarrierException,TimeoutException{finalReentrantLocklock=this.lock;lock.lock();try{//获取barrier的当前“世代”,也就是当前周期finalGenerationg=generation;if(g.broken)thrownewBrokenBarrierException();if(Thread.interrupted()){breakBarrier();thrownewInterruptedException();}//线程每调用一次await方法,就会减少1intindex=--count;if(index==0){//trippedbooleanranAction=假;尝试{finalRunnablecommand=barrierCommand;//newCyclicBarrier传入的barrierCommand和command.run()方法是同步的。如果耗时较长,则需要考虑是否异步执行。if(command!=null)command.run();ranAction=true;//这个方法1.唤醒所有阻塞的线程,2.重置计数(每来一个线程计数就会减1)并生成,以便在下一个循环中。nextGeneration();return0;}finally{if(!ranAction)breakBarrier();}}//loopuntiltripped,broken,interrupted,ortimedoutfor(;;){try{//输入if条件,说明是awaitif(withouttimeout!timed)//当前线程会释放锁,然后进入跳闸条件队列的尾部,然后自己挂起,等待被唤醒。trip.await();elseif(nanos>0L)//表示当前线程调用await方法时,指定超时时间!nanos=trip.awaitNanos(nanos);}catch(InterruptedExceptionie){//节点在条件队列中时收到中断信号会抛出中断异常!//g==generation成立,说明当前代没有变化。//!g.broken如果当前generation没有被broken,那么当前线程会break并抛出异常..if(g==generation&&!g.broken){breakBarrier();throwie;}else{//We'reabouttofinishwaitingevenifwehadnot//beeninterrupted,sothisinterruptisdeemedto//"belong"tosubsequentexecution.//执行到else多少种情况?//1。一代人变了。这时候就不需要抛出中断异常了,因为生成已经更新了,这里醒来之后就是正常的逻辑了。设置中断标志即可。//2。世代未变,世代已断。这个时候不需要返回中断异常。当执行到下面的时候,会抛出brokenBarrier异常。还要注意中断标志位。Thread.currentThread().interrupt();}}//唤醒后,执行到这里,有多少种情况?//1。正常情况下,当前barrier开启一个新的generation(trip.signalAll())//2。currentGenerationbroken,此时所有挂在行程上的线程都会被唤醒。//3。当前线程行程等待超时,然后主动转入阻塞队列,然后获取锁唤醒。if(g.broken)thrownewBrokenBarrierException();//醒来后,执行到这里,有多少种情况?//1。一般情况下,当前barrier开启新的生成(trip.signalAll())//2。当前线程行程等待超时,然后主动转移到阻塞队列中获取锁唤醒。if(g!=generation)returnindex;//醒来后,执行到这里,一共有多少种情况?//。当前线程行程等待超时,然后主动转移到阻塞队列中,然后获取锁唤醒。if(timed&&nanos<=0L){breakBarrier();thrownewTimeoutException();}}}finally{lock.unlock();}}总结至此,我们可以知道为什么CyclicBarrier可以进行循环计数了吗?CyclicBarrier使用一个内部类Generation来维持当前的循环。每个await方法都会存储当前代,从同一代获取的对象属于同一组。每当计数次数用完,就会创建一个新的Generation,并将count的值重置为parties,这意味着进入Nexttime一个新的循环。从这个await方法中是不是可以知道,只要有一个线程被中断,当前这一代的broken就会被设置为true,那么其他线程也会抛出BrokenBarrierException。相当于一个失败,另一个也必须失败,感觉是“强一致性”。总结CountDownLanch就是给计数器设置一个值。当多次执行countdown,计数器归0时,所有线程都被唤醒,CountDownLanch就失效了,只能使用一次。CyclicBarrier也是在count为0的时候唤醒所有的线程,同时重置parties的count并更新一个generation,实现复用。本文转载自微信?「java财经」,可通过以下二维码关注。转载本文请联系java财经公众号。