一、 Fork-Join
相对于显示去new 一个线程(使用Thread)其实我们在实际工作中更多的使用线程池。当然除了线程池还可以使用forkjoin。只要我们遵循了forkjoin的开发模式,就可以写出很好的多线程并发程序。
刚开始使用forkjoin 的时候其实有个疑问,就是javaa 除了提供了ThreadPoolExecutor 之外还提拱了ForkJoinPool,这个类跟ThreadPoolExecutor 类大体相同,那么实际应用中该如何选择呢?
其实这两个池都是实现ExecutorService,说白了其实是同源的,那有啥区别呢?
ThreadPoolExecutor 是单纯的线程池,更像是通用。
ForkJoinPool 这个是一个封装了分而治之玩法的工具类。使用的范围不一样,是为需要多线程分治提供的;
那我们怎么使用fork-join 呢?
其实fork-join 也跟wait/notity 一样有基本的使用范式
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.RecursiveTask;
/**
* fork-join 使用范式演示
*
* @author ckj
*
*/
public class ForkJoinPoolDemo {
/*
* 使用fork-join时不需要返回值继承RecursiveAction类 需要实现compute 方法判断任务是否足够小, 如果 足够小就直接执行任务。
* 如果不足够小,就必须分割成两个子任务,每个子任务 在调用 invokeAll 方法时, 又会进入compute 方法, 看看当前子任务是否需要继
* 续分割成孙任务,如果不需要继续分割,则执行当前子任务并返回结果。 使用 join方法会等待子任务执行完并得到其结果。
*/
static class MyForkJoinAction extends RecursiveAction {
@Override
protected void compute() {
}
}
/*
* 使用fork-join时需要返回值继承MyForkJoinTask类。 需要实现compute 方法判断任务是否足够小, 如果 足够小就直接执行任务。
* 如果不足够小,就必须分割成两个子任务,每个子任务 在调用 invokeAll 方法时, 又会进入compute 方法, 看看当前子任务是否需要继
* 续分割成孙任务,如果不需要继续分割,则执行当前子任务并返回结果。 使用 join方法会等待子任务执行完并得到其结果。
*/
static class MyForkJoinTask extends RecursiveTask<String> {
@Override
protected String compute() {
return "hello fork/join";
}
}
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
/*
* 通常需要定义一个ForkJoin任务,提供了fork 和join 的工作机制
* 通常我们不使用ForkJoinTask,而是直接使用他的子类
* 1、RecursiveAction 用户没有返回结果的任务
* 2、RecursiveTask 用于有返回值的任务
*
*/
// 无返回值
MyForkJoinAction action = new MyForkJoinAction();
// 有返回值
MyForkJoinTask task = new MyForkJoinTask();
pool.submit(task);
//获得结果 只有任务定义为RecursiveTask 才有返回结果
String result = task.join();
System.out.println(result);
}
看到代码我们其实知道了,其实使用forkjoin 的话重点需要放在compute() 方法中,只有写好compute()我们才能真正使用好forkjoin。
其实想使用好forkjoin 需要好好了解下递归的压栈与出栈,可以去网上找找归并排序的写法。也可以通过开发工具(这边使用SpringTools)打开debug 视图一步步跟踪去查看压栈与出栈的过程。
下面演示使用forkjoin 统计文件数量:
1、使用单线程统计
import java.io.File;
/**
* 单线寻找所有文件
*
* @author ckj
*
*/
public class SingleFindFile {
private static int findFile(File fileDir,int num) {
File[] files = fileDir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
// 对每个子目录都新建一个子任务。
num = findFile(file,num);
} else {
// 遇到文件,检查。
if (file.getAbsolutePath().endsWith("txt")) {
// System.out.println("文件:" + file.getAbsolutePath());
num++;
}
}
}
}
return num;
}
public static void main(String[] args) {
File fileDir = new File("D:/");
long start = System.currentTimeMillis();
int num =findFile(fileDir,0);
long end = System.currentTimeMillis();
System.out.println("单线程查询所有txt文件耗时:"+(end-start)+"文件数量为:"+num);
}
单线程查询所有txt文件耗时:20246文件数量为:2596
2、forkjoin统计
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
/**
* 通过ForkJoin 获取指定目录下所有的文件夹
*
* @author ckj
*
*/
public class ForkJoinFindFile {
/**
* 定义一个有返回值的
*
* @author asus
*
*/
static class ForkJoinTask extends RecursiveTask<List<String>> {
private File fileDir;
public ForkJoinTask(File file) {
this.fileDir = file;
}
@Override
protected List<String> compute() {
List<ForkJoinTask> subTasks = new ArrayList<ForkJoinTask>();
List<String> result = new ArrayList<String>();
File[] files = fileDir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
// 对每个子目录都新建一个子任务。
subTasks.add(new ForkJoinTask(file));
} else {
// 遇到文件,检查。
if (file.getAbsolutePath().endsWith("txt")) {
result.add(file.getAbsolutePath());
// System.out.println("文件:" + file.getAbsolutePath());
}
}
}
if (!subTasks.isEmpty()) {
// 在当前的 ForkJoinPool 上调度所有的子任务。
Collection<ForkJoinTask> invokeAll = invokeAll(subTasks);
for (ForkJoinTask subTask : invokeAll) {
result.addAll(subTask.join());
}
}
}
return result;
}
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
// 传入文件夹路径
ForkJoinTask task = new ForkJoinTask(new File("D:/"));
pool.submit(task);
List<String> join = task.join();
long end = System.currentTimeMillis();
System.out.println("forkJoin查询所有txt文件耗时:" + (end - start) + " 查询数量为:" + join.size());
}
}
forkJoin查询所有txt文件耗时:6265 查询数量为:2596
可以看到效率明显提升了。
ForkJoinPool 既然是个线程池,他定义的线程数量时多少?
这边使用Java 虚拟机线程系统的管理接口来查看下,将main 方法修改成如下所示
public static void main(String[] args) {
//Java 虚拟机线程系统的管理接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long start = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
// 传入文件夹路径
ForkJoinTask task = new ForkJoinTask(new File("D:/"));
pool.submit(task);
List<String> join = task.join();
// 不需要获取同步的monitor和synchronizer信息,仅仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos =
threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程ID和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] "
+ threadInfo.getThreadName());
}
long end = System.currentTimeMillis();
System.out.println("forkJoin查询所有txt文件耗时:" + (end - start) + " 查询数量为:" + join.size());
}
打印结果
[21] ForkJoinPool-1-worker-0
[20] ForkJoinPool-1-worker-7
[19] ForkJoinPool-1-worker-6
[18] ForkJoinPool-1-worker-5
[17] ForkJoinPool-1-worker-4
[16] ForkJoinPool-1-worker-3
[15] ForkJoinPool-1-worker-2
[14] ForkJoinPool-1-worker-1
[8] JDWP Command Reader
[7] JDWP Event Helper Thread
[6] JDWP Transport Listener: dt_socket
[5] Attach Listener
[4] Signal Dispatcher
[3] Finalizer
[2] Reference Handler
[1] main
可以看到有8个ForkJoinPool worker 线程,那么他是不是一个固定值呢?
查看ForkJoinPool的构造方法:
public ForkJoinPool() {
this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
defaultForkJoinWorkerThreadFactory, null, false);
}
发现了Runtime.getRuntime().availableProcessors() ,此方法是获取cpu核心线程数,所以在我的电脑上是8。那Doug Lea为什么要这样设计呢?
这边就提到了一个线程池定义核心线程数的业内参考方法了
1、cpu密集型(多复杂计算,逻辑处理):核心线程数设置为cpu核心线程数 ,只是一个参考值
2、io密集型(cpu 使用底程序中会存在大量I/O操作):((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目 ,但是我一般设置为 cpu核心线程数*2
3、复合型:这个没有参考值,需要压力测试来辅助测试
上述的都是参考值而已,一般是需要压力测试和业务分析后来设置的。
forkjoin还有一个非常重要的一点:工作密取
即当前线程的 Task 已经全被执行完毕,则自动取到其他线程的 Task 池中取 出 Task 继续执行。
概念的东西不想说太多, 知道有这个东西就可以。