Part5:Java并发框架Executor
jdk1.5加入的并发框架
并行的难点:任务分配和执行过程高度耦合
- 如何控制粒度,切割任务
- 如何分配任务给线程,监督线程执行过程
并行模式
- 主从模式(Master-Slave)
- Worker模式(Worker-Worker)
Java对多线程并行的管理
- ThreadGroup线程组管理
- Executor框架
- Fork-in框架
线程组ThreadGroup
尽量避免使用
Executor框架
共享线程池框架
jdk5之后开始提供,包含在java.util.concurrent.中
为以下问题提供了解决方案:
- 如何分离任务分配和执行过程
- 如何实现线程重复利用,减少创建线程带来的性能消耗
引入共享线程池的概念
- 预先设置好多个Thread,数量可弹性增减
- 线程池中每个线程多次执行很多很小的任务
- 任务的创建和执行过程(线程池)解耦
- 程序员无需关系线程池执行任务过程
Executor框架中的主要类
-
ExecutorService 线程池服务
-
ThreadPoolExexcutor
由Executors.newCachedThreadPool/newFixedThreadPool创建线程池,其中CachedThreadPool是动态增长线程池,FixedThreadPool是固定个数线程池
- Callable 线程接口
Callable和Runnable等价,可以用来执行一个任务,Callable接口需实现Call方法,注意Callable是有返回值的
- Future 返回结果
多线程运行完后,执行的结果放在Future里
例子:利用Executor框架完成1-1000的累加计算
思路:将1-1000的累加分解成10个任务,每个任务分别计算[p,q]区间内的累加,最后把每个子任务的计算结果累加,得出结果
采用主从模式
-
主任务:创建子任务并监督子任务执行
-
创建线程池:为了演示,采用固定线程池(一般指定线程的个数是cpu核数的两倍)
-
创建子任务,将子任务提交给线程池
-
获得每个子任务的计算结果,存储在Future类型的线性表里
-
轮询等待十个子任务计算完
-
获得十个子任务的计算结果,累加得出答案
-
-
子任务:完成[p,q]区间内的累加
- sum += i
子任务类
//Todo:实现Callable接口
import java.util.concurrent.Callable;
public class SumTask implements Callable<Integer>{
private int startNumber;
private int endNumber;
public SumTask(int startNumber,int endNumber){
this.startNumber=startNumber;
this.endNumber=endNumber;
}
@Override
//⚠️ call()接口是有返回值的
public Integer call() throws Exception{
int sum = 0;
for(int i=startNumber; i<=endNumber; i++)
sum += i;
return sum;
}
}
主任务类
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
public class SumMain{
private ThreadPoolExecutor executor;
private List<Future<Integer>> resultList;
private int sum;
private int taskNum = 10;
private int taskLength;
public SumMain(int sum){
//初始化4个线程的执行线程池
executor = (ThreadPoolExecutor)Executors.newFixedThreadPool(4);
//结果数组
resultList = new ArrayList<>();
//任务
this.sum = sum;
taskLength = sum/taskNum;
}
public int mainTask(){
//分成10个任务计算,提交任务
for(int i=0; i<taskNum;i++){
//创建子任务
SumTask calculator = new SumTask( i*taskLength+1, (i+1)*taskLength );
//将子任务提交给线程池,并获取子任务对象
//注意⚠️:这里获取的result对象并不一定执行完了子任务,需要通过result.isDone()询问
Future<Integer> result = executor.submit(calculator);
//纳入结果数组
resultList.add(result);
}
//调用监视器,等待10个子线程执行完毕
int total = 0;
if(serverMonitor()){
for(Future<Integer> result : resultList){
try{
//注意⚠️ Future对象的get方法会抛出InterruptedException 和ExecutionException
int tmp = result.get();
total += tmp;
}catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
// 关闭线程池
executor.shutdown();
return total;
}
private boolean serverMonitor(){
// 每隔50毫秒,轮询等待10个任务结束
do {
System.out.printf("Main: task complied: %d\n",executor.getCompletedTaskCount());
for (int i=0; i<resultList.size(); i++) {
Future<Integer> result=resultList.get(i);
System.out.printf("Main: Task %d: %s\n",i,result.isDone());
}
} while (executor.getCompletedTaskCount()<resultList.size());
return true;
}
}
Part6: Fork-Join框架
采用递归的思想,对问题分而治之:分解、治理、合并;
与Executor框架的区别,适合用于整体任务量不好确定的场合
Java 7之后引入
Fork-Join框架中的关键类
-
ForkJoinPool 任务池
-
RecursiveAction
用于没有返回结果的任务
- RecursiveTask
用于有返回结果的任务,继承RecursiveTask类后必须实现compute方法,同样的compute方法可以有返回值
例子:利用Fork-Join框架完成1-1000的累加计算
该文概括Fork-Join框架的工作线程池:异步多线程----Fork-Join框架
该文详细讲解Fork-Join框架的工作线程池:Java Fork-Join 模型正确打开方式
一些接口的区别:ForkJoinPool invoke、execute和submit区别
思路:在讲解Executor框架的例子中,可以看到解决这个累加问题的前提是已知问题的规模。在主任务中,我们指定了子任务的规模 taskNum = 10, 这本质是一种循环的思路:已知循环次数,在循环内实现累加求和。
Fork-Join框架将采用递归的思路来解决问题。对于递归来说,解决问题关键点有两个:
- 问题的出口,即求解的元操作
- 如何缩小问题的规模
依然采用主从模式:
- 主任务:创建问题,提交问题,等待结果
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
public class SumMain{
private ForkJoinPool pool;
private int startNum;
private int endNum;
private int total = 0;
public SumMain(int startNum,int endNum){
//创建执行线程池,未指定线程池规模
pool = new ForkJoinPool();
this.startNum = startNum;
this.endNum = endNum;
}
public int mainTask(){
//创建任务
SumTask task = new SumTask(startNum, endNum);
//提交任务给线程池
ForkJoinTask<Integer> result = pool.submit(task);
//等待结果
if(serverMonitor(result)){
try{
total = result.get();
}catch(InterruptException e1){
e1.printStackTrace();
}catch(ExecutionException e2){
e2.printStackTrace();
}
}
return total;
}
private boolean serverMonitor(ForkJoinTask<Integer> result){
//轮询50毫秒等待结果
//等待结果
do {
System.out.printf("Main: Thread Count: %d\n",pool.getActiveThreadCount());
//getParallelism(), 查看当前pool的并行处理的线程数字
System.out.printf("Main: Paralelism: %d\n",pool.getParallelism());
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (!result.isDone());
return true;
}
}
- 子任务:用递归解决
import java.util.concurrent.RecursiveTask;
//分任务求和,存在返回值,所以继承RecursiveTask
public class SumTask extends RecursiveTask<Long> {
public static final int threadhold = 5;
private int start;
private int end;
public SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
Long sum = 0L;
// 元操作:在阈值以内的个数,直接循环累加
boolean canCompute = (end - start) <= threadhold;
if (canCompute) {
for (int i = start; i <= end; i++) {
sum = sum + i;
}
} else {
// 划分任务:任务大于阈值, 分裂为2个任务
int middle = (start + end) / 2;
SumTask subTask1 = new SumTask(start, middle);
SumTask subTask2 = new SumTask(middle + 1, end);
invokeAll(subTask1, subTask2);
//join()会等待子任务结束之后 才返回值,若未结束,则会阻塞等待
Long sum1 = subTask1.join();
Long sum2 = subTask2.join();
// 结果合并
sum = sum1 + sum2;
}
return sum;
}
}
``