紧接着上一篇我们继续分析在并发编程中所用到的一些并发工具。
5.Fork/Join 框架
5.1 什么是 Fork/Join 框架
Fork/Join 框架是 Java7 提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
Fork :就是把一个大任务切分成若干子任务并行的执行;Join:就是合并这些子任务的执行结果。
设计思路图:
5.2 工作窃取算法
工作窃取(work-stealing)算法:某个线程从其他队列中窃取任务来执行。
在 Fork/Join 框架中为了减少线程间的竞争,把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。但是,有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时他们会访问同一个队列,所以减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
双线程工作窃取的工作流程:
工作窃取算法的优点:充分利用线程进行并行计算,减少了线程间的竞争。
工作窃取算法的缺点:在某些情况下还是存在竞争,比如双端队列只有一个任务时;算法会消耗更多的系统资源,可能会创建多个线程和双端队列。
5.3 使用 Fork/Join 框架
使用 Fork/Join 框架来完成任务大概分为两个步骤:
- 分割任务;
- 执行任务并合并结果。
Fork/Join 框架使用两个类完成以上的两件事情。
-
ForkJoinTask:我们要使用 Fork/Join 框架,必须首先创建一个 ForkJoin任务。它提供在任务中执行 fork() 和 join() 操作机制。通常情况下我们不需要直接继承 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类。
RecursiveAction:用于没有返回结果的任务。
RecursiveTask:用于有返回结果的任务。 -
ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行。
任务分割出的子任务会添加到当前工作线程多维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。
光说不练,下面我们设计一个利用 Fork/Join 框架实现的排序算法:
public class SortTask extends RecursiveTask<int[]> {
private static final int THRESHOLD = 10;
private int start;
private int end;
private int[] arrays;
public SortTask(int start, int end, int[] arrays) {
this.start = start;
this.end = end;
this.arrays = arrays;
}
@Override
protected int[] compute() {
boolean canCompute = (end - start + 1) <= THRESHOLD;
if (canCompute) {
Arrays.sort(arrays, start, end);
} else {
// 如果任务大于阈值,就分裂成两个子任务计算
int mid = (start + end) / 2;
SortTask leftTask = new SortTask(start, mid, arrays);
SortTask rightTask = new SortTask(mid+1, end, arrays);
// 执行子任务
leftTask.fork();
rightTask.fork();
// 等待子任务执行完成,得到结果
int[] leftArrays = leftTask.join();
int[] rightArrays = rightTask.join();
// 合并子任务
Arrays.sort(leftArrays, start, end);
}
return arrays;
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
SortTask sortTask = new SortTask(0, 20, new int[]{
20, 1, 3, 5, 4, 3, 6, 7, 1, 11, 19, 18, 13, 14, 15, 21, 3, 44, 3, 7});
ForkJoinTask<int[]> result = forkJoinPool.submit(sortTask);
try {
int[] ints = result.get();
for (int i = 0; i < ints.length; i++) {
System.out.print(ints[i] + " ");
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
5.4 Fork/Join 框架的实现原理
Fork/Join 框架中双端队列的相关操作与 ConcurrentHashMap 的 table 数组较为相似,都是采用 CAS 的方式来处理每个桶的并发,这里推荐两篇博客可以结合着理解 Fork/Join 框架的实现原理:
Fork/Join框架(1) 原理
Fork/Join框架(2)实现
这两篇分别从原理和源码实现来分析了Fork/Join 框架。
三、并发工具类
在JDK并发包里提供了几个非常有用的并发工具类。CountDownLatch、CyclicBarrier、Semaphore和Phaser 工具类提供了一种并发流程控制的手段,Exchanger 工具类则提供了在线程间交换数据的一种手段。
1.等待多线程完成的 CountDownLatch
CountDownLatch 允许一个或多个线程等待其他线程完成操作。
1.1 CountDownLatch 简介
CountDownLatch是一个辅助同步器类,用来作计数使用,它的作用类似于倒数计数器,先设定一个计数初始值,当计数降到0时,将会触发一些操作。
初始计数值在构造CountDownLatch对象时传入,每调用一次 countDown() 方法,计数值就会减1。
线程可以调用CountDownLatch的await方法进入阻塞,当计数值降到0时,所有之前调用await阻塞的线程都会释放。
注意:计数器必须大于等于0,计数器等于0时,调用await方法时不会阻塞当前线程。CountDownLatch 不可能重新初始化或者修改 CountDownLatch 对象的内部计数器的值。
1.2 CountDownLatch 应用场景
作为一个开关/入口
需求一:假设上学某天老师迟到(老师拿的钥匙),那么来的学生就只能在教室门外等老师,老师来了学生才能进入教室,这里老师就相当于一个教室的入口。
public class Holder {
private static final CountDownLatch startLatch = new CountDownLatch(1);
private static final int N = 5;
public static void main(String[] args) {
for (int i = 0; i < N; i++) {
new Student(startLatch).start();
}
// 主线程调用countDown方法,相当于老师来了
startLatch.countDown();
System.out.println("teach coming,open the door");
}
}
class Student extends Thread {
private final CountDownLatch waitLatch;
public Student(CountDownLatch waitLatch) {
this.waitLatch = waitLatch;
}
@Override
public void run() {
try {
// 如果count不为0,说明老师没来
if (waitLatch.getCount() != 0) {
System.out.println("student need wait");
waitLatch.await();
} else {
System.out.println("the teacher coming, student can enter");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
程序运行可能会出现打印语句顺序的问题,这是因为没有同步出现的原因,因为这不是重点,就没有给代码加锁。
作为一个事件的完成信号
需求二:使某个线程在其它N个线程完成某项操作之前一直等待。
public class Holder {
private static final int N = 5;
private static final CountDownLatch Latch = new CountDownLatch(N);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < N; i++) {
new Worker(Latch).start();
}
Latch.await();
System.out.println("work finish");
}
}
class Worker extends Thread {
private final CountDownLatch latch;
public Worker(CountDownLatch latch) {
this.latch = latch;