文章目录
1. 为什么要使用线程池?
如果放任线程无限制地创建,会耗尽CPU资源,并且降低系统的响应速度。
为了更好的对线程进行管理,实现线程的统一分配,调优和监控,降低资源消耗。
JUC设计了线程池。线程池从机制上分为两种.,一是ThreadPoolExecutor,另一类是ForkJoinPool。
- ThreadPoolExecutor
使用多个线程和阻塞队列实现对线程和任务进行管理的工具。 - ForkJoinPool
将一个大任务拆分为很多小任务来异步执行的管理的工具。
1.1 Executor 和 ExecutorService
ThreadPoolExecutor 和 ForkJoinPool 都实现了Executor 和 ExecutorService接口。
Executor 和 ExecutorService接口规定了对线程池使用的提交任务和关闭任务的方法。
1.2 线程池提交任务
execute()
方法是Executor接口规定的。只能接受Runnable类型的任务。
void execute(Runnable command);
submit()
方法是ExecutorService接口规定的。有返回值,且可以通过Future.get()抛出Exception
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
1.3 线程池关闭
-
shutdown()方法,shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断。
-
shutdownNow(),而shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回。
shutdownNow会停止正在运行的线程。
2. ForkJoinPool
2.1 ForkJoinPool原理
ForkJoinPool使用分治算法(Divide-and-Conquer)把任务递归的拆分为各个子任务,,尽可能的使用所有可用的计算能力来提升应用性能。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pIbUykHP-1647316188651)(https://s2.loli.net/2022/03/15/YMZaRk17vzgGxVJ.png)]
2.2 ForkJoinTask
ForkJoinPool 只接收 ForkJoinTask 任务。我们一般都会继承 RecursiveTask
、RecursiveAction
或 CountedCompleter
来实现我们的业务需求,而不会直接继承 ForkJoinTask 类。
- RecursiveTask 是 ForkJoinTask 的子类,是一个可以递归执行的 ForkJoinTask。
- RecursiveAction 是一个无返回值的 RecursiveTask。
- CountedCompleter 在任务完成执行后会触发执行一个自定义的钩子函数。
2.3 继承实现一个RecursiveTask
继承RecursiveTask 需要 重写其compute()
方法,该方法的返回值是泛型。
当任务较大的时候 就将任务切分,当任务规模能够直接处理就在本地处理,并返回结果。
RecursiveTask2
类,是自定义的实现简单的数字累加。当计算规模在100个数以内,就直接使用循环计算。如果大于100个数,将划分为两个部分,并创建两个RecursiveTask2
对象fork()后再join()。
public class RecursiveTask2 extends RecursiveTask<Integer> {
int start,end;
static final int MAXNUM=100;
public RecursiveTask2(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if(end-start>MAXNUM) {
int mid = start+(end-start)/2;
RecursiveTask2 task1 = new RecursiveTask2(start,mid);
RecursiveTask2 task2 = new RecursiveTask2(mid,end);
task1.fork();
task2.fork();
return task1.join()+task2.join();
}
else {
int sum = 0;
while (start<end) {
start++;
sum+=start;
}
return sum;
}
}
}
2.4 使用ForkJoinPool计算RecursiveTask任务
将自定义的任务提交到ForkJoinPool。ForkJoinPool将不断创建子线程任务,直到每个线程能计算的任务规模在预定的规模下,然后再汇总计算结果。
public class ForkJoinPoolTest2 {
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
RecursiveTask2 recursiveTask2 = new RecursiveTask2(0, 1000);
forkJoinPool.execute(recursiveTask2);
System.out.println(recursiveTask2.join());
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
500500
2.5 WorkStealingPool
Executors工具类也提供了创建ForkJoinPool的方法。源码如下:
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
WorkStealingPool是一类比较特殊的ForkJoinPool。每个线程分到自己的任务后,如果发现自己的任务已完成那么它可以“窃取”其他线程未完成的任务。进一步提升了效率和使用率。其原理如下
-
每个线程都有自己的一个WorkQueue,该工作队列是一个双端队列。划分的子任务调用fork时,都会被push到自己的队列中。
-
所有者线程调用pop方法,从WorkQueue提取任务。
-
当线程完成了任务,自己所属的WorkQueue为空。则随机从另一个线程的队列末尾调用poll方法窃取任务。
public class ForkJoinPoolTest3 {
public static void main(String[] args) {
ExecutorService forkJoinPool = Executors.newWorkStealingPool(20);
for(int i=0;i<100;i++) {
forkJoinPool.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
forkJoinPool.shutdown();;
}
}
总结
ForkJoinPool使用分治算法(Divide-and-Conquer)把任务递归的拆分为各个子任务,,尽可能的使用所有可用的计算能力来提升应用性能。
多线程系列在github上有一个开源项目,主要是本系列博客的实验代码。
https://github.com/forestnlp/concurrentlab
如果您对软件开发、机器学习、深度学习有兴趣请关注本博客,将持续推出Java、软件架构、深度学习相关专栏。
您的支持是对我最大的鼓励。