CountDownLatch和CyclicBarrier都能在线程执行时设置中间等待位置,表面上看相似度很高,下面讲讲他们的区别:
CountDownLatch是线程A等待B。它有着明确的顺序,A先执行,到中间等待位置后,A调用countDown()方法,激活B,B才开始执行。再强调一下,这个类强调顺序,A先,B后
CyclicBarrier是线程间互相等待,即A和B互相等待,可能是A等待B,也可能是B等待A。举个例子,当年纣王对苏妲己吟过一首诗:爱妃听我真心言,你我想约共百年,谁若九十七岁死,奈何桥上等三年。这种情况就必须使用CyclicBarrier来实现。中间等待位置是奈何桥上,纣王和苏妲己是2个线程,他们活在人世时是线程的前半部分,相约100岁时在奈何桥见面,然后牵手走完线程的后半部分。但人有旦夕祸福,两人都能活到100岁吗?不一定,可能纣王命短,97岁就死了,于是他到奈何桥头等待妲己。反之也有可能妲己命短,她97岁死了,去奈何桥等纣王3年。这就是所谓的相互等待,谁等谁都是有可能的。并且,两人在奈何桥会和后,线程并未结束,二人还会手牵手继续前行,走完线程的后半部分。
CyclicBarrier还有一个重要特点,他是可重用的,当计数器减少到0开闸后,计数器会自动地恢复为初始值,以便下一次使用。
CyclicBarrier应用举例:多线程实现希尔排序。以长度为10的数组{3,6,7,1,2,9,5,12,10,16}为例,开3个线程处理。
当步长为5时,原数组被分为5个子数组,分别是:
3, 9 6, 5 7, 12 1, 10 2, 16用3个线程对着5个子数组进行插入排序,第一个线程负责{3,9}和{1,10}两个子数组;第二个线程负责{6,5}和{2,16}两个子数组;第三个线程负责{7,12}这一个子数组。三个线程分开运行,互不干扰,无需加锁同步。使用CyclicBarrier让他们都在排序结束后相互等待。当3个线程全部排序完毕,修改步长为2,然后3个开工继续对子数组进行插入排序。这时有个问题,线程有3个,子数组只有2个,因此,第三个线程将什么都不做,直接执行await()语句进入等待状态。以此类推,直到步长为1排序结束后。整个数组排序结束,3个线程也自然结束。
以下是源码,定义了一个MultiThreadAlgorithm类,有一个静态方法shellSort(),在该方法中创建多个线程,进行希尔排序。最后定义了线程对象,负责具体的排序操作。此代码使用了Java 7的语法
//多线程算法对象,内部定义了一些静态方法,均用多线程实现 public class MultiThreadAlgorithm { public static void shellSort(int[] a, int threadCount) { List<Integer> gaps = new ArrayList<>(); CyclicBarrier barrier = new CyclicBarrier(threadCount); CountDownLatch latch = new CountDownLatch(threadCount); int gap = a.length/2; while(gap>=1) { //希尔排序步长的选择是最基本的... 8,4,2,1 gaps.add(gap); gap /= 2; } for(int i=0; i<threadCount; i++) { new ShellSortThread(i, threadCount, barrier, gaps, latch, a).start(); } try { latch.await(); //在多线程排序时,用此门闩将主线程阻塞,直到排序结束 } catch (InterruptedException e) { e.printStackTrace(); } } } //负责排序的线程 class ShellSortThread extends Thread { private int threadID; //当前线程的编号,取值为 0, 1, 2, 3 ... private int threadSum; //开启的线程总数 private CyclicBarrier barrier; //设置执行中间点 private List<Integer> gaps; //希尔排序的步长序列 private CountDownLatch latch; //用于阻塞主线程,与希尔排序算法无关 private int[] a; //待排序的数组 public ShellSortThread(int threadID, int threadSum, CyclicBarrier barrier, List<Integer> gaps, CountDownLatch latch, int[] a) { super(); this.threadID = threadID; this.threadSum = threadSum; this.barrier = barrier; this.gaps = gaps; this.latch = latch; this.a = a; } /* * 算法思想:希尔排序原理是利用步长将原序列划分为多个子序列,对每个子序列进行插入排序 * 例如,若步长为8,则划分为8个子序列,并且这些子序列是互不相关的, * 因此,可以使用多线程来对这些子序列进行插入排序,不存在同步问题。 * 例如,开6个线程进行排序,当步长为20时, * 第1个线程负责第1,7,13,19个子序列的排序, * 第2个线程负责第2,8,14,20个子序列的排序, * ...... * 第6个线程负责第6,12,18个子序列的排序 * 每个线程使用当前步长排序结束后,调用CyclicBarrier对象的await()方法等待其他线程 * 当所有线程都使用当前步长排序结束后,CyclicBarrier闸门打开,更改步长,重复上面的步骤排序 * 当步长减少到1时,只有第1个线程进行插入排序,其他线程空转,直接await() * 最后,排序结束 */ @Override public void run() { for(int i=0, gap; i<gaps.size(); i++) { gap = gaps.get(i); for(int sub_array_index = threadID; sub_array_index<gap; sub_array_index += threadSum) { for(int j=sub_array_index+gap; j<a.length; j += gap) { int temp = a[j]; int k; for(k=j-gap; k>=0 && a[k] > temp; k -= gap) { a[k+gap] = a[k]; } a[k+gap] = temp; } } try { barrier.await(); } catch (InterruptedException | BrokenBarrierException e) { //若发生异常,必须修改latch后退出,否则主线程会永远阻塞 latch.countDown(); return; } } latch.countDown(); } }