多线程之Fork/Join

915 阅读3分钟

多线程之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里的每个Username后面拼接___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有两种方法,executeinvoke

  • 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的长度是否满足切分的大小,满足就执行,不满足就继续切分。

而这里有两个方法

  • invokeAll
  • join 当任务执行完成后,返回计算的结果

看一下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);
}

这里有forkdoInvokedoJoin方法。

  • fork当前线程是ForkJoinWorkerThread,加入ForkJoinPoolworkQueue中,等待执行,不是则调用ForkJoinPool.common.externalPush(this);
  • doInvoke 先尝试本线程执行,不成功才走后续流程,当前线程是ForkJoinWorkerThread时不尝试将该task移出栈并执行,而是等待。当前线程不是ForkJoinWorkerThread,调用externalAwaitDone方法。
  • doJoin 已完成,返回status。未完成,当前线程是ForkJoinWorkerThread,从该线程中取出workQueue,并尝试将当前task出队然后执行,执行的结果是完成则返回状态,否则使用当线程池所在的ForkJoinPoolawaitJoin方法等待。当前线程不是ForkJoinWorkerThread,调用前面说的externalAwaitDone方法。

总结

ForkJoin用一种新的方式来使用多线程处理任务,分而治之,化整为零。基于窃取算法,高效的利用CPU,终极目标就是跑满CPU,适合计算密集型任务。

以前遇到过一个需求,大量的规则校验,还有计算,单线程需要10秒多,使用ForkJoin只用100多毫秒,然后再加上缓存,瞬间美滋滋。

看一下效果:

使用ForkJoin

使用lambda表达式

优化不是很明显,但是越是计算密集型任务,优化越明显,这个修改名字的任务还是太简单了。

参考:

segmentfault.com/a/119000001…

最后欢迎大家关注我的公众号,共同学习,一起进步。加油🤣

搜索关注:南诏Blog