多线程之Fork/Join
Java7中提供了一种新的处理多线程任务的方式:Fork/Join,采用ForkJoinPool来将一个任务拆分成多个"小任务"并行计算,然后再把多个"小任务"的结果合并成总的计算结果。一句话概括就是化整为零,分而治之。
首先 ForkJoinPool
使用Fork/Join需要创建一个ForkJoinPool。
在Java7中使用构造器的方式,如ForkJoinPool pool = new ForkJoinPool(6);,而6是并行处理器的数量,一般设置为CPU核心数,因为我的是6核,所以这里是6。
如果设置为2的话,就是下面这样:
Java8之后,可以使用ForkJoinPool pool = ForkJoinPool.commonPool();这种方式获取ForkJoinPool。
查看源码可以知道,是在java.util.concurrent.ForkJoinPool#makeCommonPool这个方法里创建的ForkJoinPool,主要是这一句:
return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE,
"ForkJoinPool.commonPool-worker-");
前面是获取电脑的配置,但是没有获取到,所以是11。
接下来 Task
有了ForkJoinPool,接下来就可以处理任务啦。任务就是ForkJoinTask,但是我们一般不使用这个类,而是使用它的子类。区别如下:
RecursiveAction<V>不需要任务返回结果RecursiveTask<V>需要任务返回结果
示例
功能:在list里的每个User的name后面拼接___changed。
测试实体类
@Data
public class User {
private String name;
public User(String name) {
this.name = name;
}
}
public class ForkJoinDemo {
private List<User> addString() {
List<User> list = new ArrayList<>();
list.add(new User("xiao"));
list.add(new User("hua"));
list.add(new User("zhao"));
list.add(new User("li"));
list.add(new User("liu"));
list.add(new User("sun"));
list.add(new User("zhou"));
list.add(new User("wu"));
list.add(new User("zheng"));
list.add(new User("wang"));
//return list.stream().peek(e -> e.setName(e.getName() + "__changed")).collect(Collectors.toList());
// ForkJoinPool
ForkJoinPool pool = new ForkJoinPool(6);
TaskDemo task = new TaskDemo(list);
//pool.execute(task);
return pool.invoke(task);
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
ForkJoinDemo demo = new ForkJoinDemo();
List<User> list = demo.addString();
System.out.println(JSONObject.toJSONString(list));
long time = System.currentTimeMillis() - start;
System.out.println("ForkJoinDemo_main_time:{} " + time + "ms");
}
}
这里是创建一个ForkJoinPool,然后把任务扔给ForkJoinPool处理,
而调用ForkJoinPool有两种方法,execute和 invoke。
execute没有返回值invoke有返回值
ForkJoinPool pool = new ForkJoinPool(6);
TaskDemo task = new TaskDemo(list);
//pool.execute(task);
return pool.invoke(task);
接下来是处理的逻辑:
public class TaskDemo extends RecursiveTask<List<User>> {
private List<User> list;
private static final int MIN_LENGTH = 1;
TaskDemo(List<User> list) {
this.list = list;
}
@Override
protected List<User> compute() {
List<User> listResult = new ArrayList<>();
if (list.size() <= MIN_LENGTH) {
return chengeName(list);
} else {
TaskDemo task1 = new TaskDemo(list.subList(0, MIN_LENGTH));
TaskDemo task2 = new TaskDemo(list.subList(MIN_LENGTH, list.size()));
invokeAll(task1, task2);
listResult.addAll(task1.join());
listResult.addAll(task2.join());
}
return listResult;
}
private List<User> chengeName(List<User> list) {
for (User user : list) {
user.setName(user.getName() + "___changed");
System.out.println(Thread.currentThread().getName() + " " + user.getName());
}
return list;
}
}
这里使用构造器的方式,用一个成员变量来接收,传过来需要处理的数据。
修改的方法写在chengeName这里,而能够化整为零,分而治之的核心就是compute方法。
在执行compute方法内部逻辑时,会先判断list的长度是否满足切分的大小,满足就执行,不满足就继续切分。
而这里有两个方法
invokeAlljoin当任务执行完成后,返回计算的结果
看一下invokeAll方法
public static void invokeAll(ForkJoinTask<?> t1, ForkJoinTask<?> t2) {
int s1, s2;
t2.fork();
if ((s1 = t1.doInvoke() & DONE_MASK) != NORMAL)
t1.reportException(s1);
if ((s2 = t2.doJoin() & DONE_MASK) != NORMAL)
t2.reportException(s2);
}
这里有fork,doInvoke,doJoin方法。
fork当前线程是ForkJoinWorkerThread,加入ForkJoinPool的workQueue中,等待执行,不是则调用ForkJoinPool.common.externalPush(this);。doInvoke先尝试本线程执行,不成功才走后续流程,当前线程是ForkJoinWorkerThread时不尝试将该task移出栈并执行,而是等待。当前线程不是ForkJoinWorkerThread,调用externalAwaitDone方法。doJoin已完成,返回status。未完成,当前线程是ForkJoinWorkerThread,从该线程中取出workQueue,并尝试将当前task出队然后执行,执行的结果是完成则返回状态,否则使用当线程池所在的ForkJoinPool的awaitJoin方法等待。当前线程不是ForkJoinWorkerThread,调用前面说的externalAwaitDone方法。
总结
ForkJoin用一种新的方式来使用多线程处理任务,分而治之,化整为零。基于窃取算法,高效的利用CPU,终极目标就是跑满CPU,适合计算密集型任务。
以前遇到过一个需求,大量的规则校验,还有计算,单线程需要10秒多,使用ForkJoin只用100多毫秒,然后再加上缓存,瞬间美滋滋。
看一下效果:
使用ForkJoin
使用lambda表达式
优化不是很明显,但是越是计算密集型任务,优化越明显,这个修改名字的任务还是太简单了。
参考:
最后欢迎大家关注我的公众号,共同学习,一起进步。加油🤣
搜索关注:南诏Blog