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();
}
}