了解Fork/Join这个框架是因为我在工作上遇到了一个需求,需要处理一个百万以上的数据。一开始我没有做任何的优化,第一版去测试的时候,差不多40万的数据,运行了50分钟,因为客户的需求是每天都要定时运行一次,那这样肯定是不行。然后我就去问我导师怎么搞,他就推荐我使用Fork/Join去做。先说结果,速度提升了10倍,5分钟就跑完了。

言归正传,我们来开始了解一下Fork/Join这个框架。

  1. Fork/Join

Fork/Join框架是Java7提供的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。使用工作窃取(work-stealing)算法,主要用于实现“分而治之”。

但我觉得可以直接把它当做递归来思考,因为你在真正用Fork/Join的时候也是跟递归是一样的。对递归还不熟悉的同学建议先去了解一下递归再来学习Fork/Join。因为它其实就是把大任务分解成一个个小的任务,然后建立一个线程池,线程池里面的线程并发去处理这些个小任务。

  2.方法

fork() 就是执行子任务

join()方法的主要作用是阻塞当前线程并等待获取结果,所以Join()是用在有返回值的情况里的

invokeAll() 这个是将两个分解出来的子任务一起执行,执行子任务调用fork方法并不是最佳的选择,最佳的选择是invokeAll方法。用我下面贴的代码来举例就是

invokeAll(leftTask, rightTask);
等同于
leftTask.fork();
rightTask.fork();

 

 

  3.实例

在我上面说到我工作遇到的事例就是,我要对一个百万以上的数据做同步操作。那么我就是将List分解成一个个小List,然后每个线程去遍历这个小的List将每个List里面的数据去做同步操作。(我这里是有一个同步的api,我只需要将每个数据拿出来,然后调用这个接口就可以了)

java如何实现百万用户同时在线 java 百万并发_线程池

 

 

举个栗子,假设这个List的size是9,我要去分割这个List,我先设定最小的任务的大小是2,那么当分割到任务小于或等于2的时候,就不会继续分割下去了。如图所示,我最后分割成了5个小任务,然后多个线程并发去遍历这几个小任务。

  3.工作上用到的代码

上面这个例子,大伙可以自己去摸索一下代码要怎么写,我就懒得写了,要春节了,偷懒一下。下面我贴一下我工作上写的代码。我这里去分割List是使用下标start和end去分割的,我一开始还傻傻的以为要去new一个List,把自己给蠢到了。

ps:ColumnSet是自己包装的类型

       AddressBookDBSync就是同步的类

       这里继承的是RecursiveAction,还可以继承RecursiveTask。前者适用于没有返回值的场景,而后者适合于有返回值的场景

public class SyncEoaAddressBookForkJoin extends RecursiveAction {
    private int minTaskSize; //最小任务大小
    private int start;
    private int end;
    private List<ColumnSet> list;
    private ConfigSection configSection;
    int totalSize;

    public SyncEoaAddressBookForkJoin(int start, int end, List<ColumnSet> list, ConfigSection configSection) {
        this.start = start;
        this.end = end;
        this.list = list;
        this.configSection = configSection;
        this.totalSize = list.size();
        minTaskSize = configSection.getItem("minTaskSize").getInt();
    }

    @Override
    protected void compute() {
        boolean executable = (end - start) > minTaskSize;
        if (!executable) {
            //直接调用同步方法
            try {
                AddressBookDBSync addressBookDBSync = new AddressBookDBSync(configSection);
                for (int i = start; i <= end ; i++) {
                    ColumnSet columnSet = list.get(i);
                    addressBookDBSync.getUserEoaUid(columnSet.getString("eoa_user_id"));
                }
            } catch (Exception e) {
                LOG.warning("request error");
            }
        } else {
            int middle = (start + end) / 2;
            SyncEoaAddressBookForkJoin leftTask = new SyncEoaAddressBookForkJoin(start, middle, list, configSection);
            SyncEoaAddressBookForkJoin rightTask = new SyncEoaAddressBookForkJoin(middle + 1, end, list, configSection);
            invokeAll(leftTask, rightTask);
        }
    }
}

 然后就是怎么去创建线程池和调用这个方法去并发处理

//调用ForkJoin去并发处理数据
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        SyncEoaAddressBookForkJoin syncEoaAddressBookForkJoin = new SyncEoaAddressBookForkJoin(0, listSize, list, configSection);
        forkJoinPool.submit(syncEoaAddressBookForkJoin);
        syncEoaAddressBookForkJoin.join(); //等待整个任务完成
        //检查报错
        if (syncEoaAddressBookForkJoin.isCompletedAbnormally()) {
            CmcuUtils.LOG.warning("forkJoin hsa error :" + syncEoaAddressBookForkJoin.getException());
        }
        //关闭线程池
        forkJoinPool.shutdown();

 

forkJoinPool.submit(syncEoaAddressBookForkJoin)就是将分割成的一个个小任务扔到线程池里面,然后最后记得关闭线程池就可以了,亲测有效。

 

 

当然,Fork/Join的知识点远远不止这点,里面还包含着它设计的工作队列、工作窃取(work-stealing)算法,只是对于我们平常工作中的使用,我们认识这么多就够了。并且网上关于这些的描述已经很完善了,“前人之述备矣”。

 https://ld246.com/article/1517841032139

   https://zhuanlan.zhihu.com/p/101418412

 

 

     4.实际使用时遇到的问题:

因为分的任务太多了,导致请求量太高被nignx拦截了。查看nignx的错误日志可以看到

2022/04/20 09:00:49 [error] 2799268#0: *179131 limiting requests, excess: 50.200 by zone "mylimit", client: xx.xxx.xxx.xx, server: , request: "POST /abcsync/contact/xxxx?param=xxx HTTP/1.0", host: "xxx.test.abc"

它这个意思就是被nignx里面的“mylimit”命中,被拦截了,所以报了 Http Status : 503

java如何实现百万用户同时在线 java 百万并发_线程池_02

 

 以上配置表示,限制每个 IP 访问的速度为 50r/s,因为 Nginx 的限流统计是基于毫秒的,我们设置的速度是 50r/s,转换一下就是 20ms 内单个 IP 只允许通过 1 个请求,从 21ms 开始才允许通过第 2 个请求。

所以我这个“mylimit”限制每秒只允许通过50个请求,但是如果我们调整“rate=50r/s”的话,服务器可能会处理不过来,也会导致报错,看网友说也可以修改那个“65m”,这个好像是缓存空间,就是如果有请求超过

这个限制量,则会被存放到这个缓存空间里面,当服务器空闲时就会重新处理被暂时搁置的请求。但是这个当时没仔细看,如果想修改这个的话,可以google一下。

最后说一下我最后是怎么解决问题的,我用了最简单的方法,我把minTaskSize调大了,这样子请求量就会降下来。因为我这个系统不可能全部都来处理我这个任务,所以只能放弃性能。但是最后处理时间也才

几十分钟,也算是完成任务了。