并行流适用场景-CPU密集型

文章目录

  • 1. 场景描述
  • 2. 原因猜测
  • 3. 测试
    • 3.1 默认并行流线程数
    • 3.2 提高并行流线程数
    • 3.3 测试结果:6w个任务
  • 4. 总结
    • 4.1 I/O密集型
    • 4.1 CPU密集型
  • 5. 参考资料

1. 场景描述

​ 每天有一次定时任务,需要向300w用户推送支付宝模板消息,单台服务器初次尝试30w个消息推送时,耗时4.6个小时,使用的是并行流推送的方式。

2. 原因猜测

​ 通过监控观察到,推送模板消息时,服务器CPU的利用率只在 10% 左右,并没有高效利用起CPU,并行流的优点是将CPU资源调动起来,实现并行处理,一般情况CPU资源应该是被占用得挺多的,假设服务器有四个核,默认并行流会将四个核都同时调用起来,实现真正的并行处理,而不像线程是伪实时,可以将四个核心下的并行流处理单成四个队列,30w任务平均分配在四个队列中,只有前面的任务执行完了,下一个任务才会继续,此时就会面临一个问题了,推送模板消息是发送http请求,涉及到了网络调用,此时每个任务执行过程中是有一个I/O阻塞时间的,下一个任务只能等待前一个完成才会执行,这段I/O阻塞便停滞了CPU资源。

​ 可以通过提高并行流的核心数或者换成线程处理的方式来验证猜测。

3. 测试

@Service
public class ConsumMsg {

    @Autowired
    StreamOpt streamOpt;

    /**
     * 初始化任务队列
     * @param count
     * @return
     */
    public List<Integer> initMsg(int count) {
        List<Integer> list = Lists.newArrayList();
        for (int i = 0; i < count; i++) {
            list.add(i);
        }
        return list;
    }

    /**
     * 并行流处理
     * @param count
     */
    public void parallel(int count) {
        List<Integer> list = initMsg(count);
        int successSize = list.parallelStream().mapToInt(streamOpt::sendMsg).sum();
        System.out.println("并行流成功个数:" + successSize);
    }

    /**
     * 线程池处理
     * @param count 
     * @param maxThreadCount 最大线程数
     * @param initThreadCount 初始线程数
     * @param latch
     * @throws ExecutionException
     * @throws InterruptedException
     */
    public void normalThread(int count, int maxThreadCount, int initThreadCount, CountDownLatch latch) throws ExecutionException, InterruptedException {
        List<Integer> list = initMsg(count);
        // 等待的其他线程则存储到队列里面
        ThreadPoolExecutor executor = new ThreadPoolExecutor(initThreadCount, maxThreadCount, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(60000));
//        int sum = 0;
        for (int i = 0; i < list.size(); i++) {
            Future result = executor.submit(new SendMsgThread(i, latch));
            // 结果转换耗时很长,需要取消
//            sum += (Integer) result.get();
        }
//        System.out.println("线程池成功个数:" + sum);
    }

    /**
     * 线程
     */
    class SendMsgThread implements Callable {

        int msg;
        CountDownLatch latch;

        public SendMsgThread(int msg, CountDownLatch latch) {
            this.msg = msg;
            this.latch = latch;
        }

        @Override
        public Integer call() throws Exception {
            int result = streamOpt.sendMsg(msg);
            latch.countDown();
            return result;
        }
    }
}

@Service
public class StreamOpt {


    public int sendMsg(int msg) {
       // todo 网络调用
        return 1;
    }

}

3.1 默认并行流线程数

public class ConsumTest  extends BaseTest {

    @Autowired
    ConsumMsg consumMsg;

    @Test
    public void test() throws InterruptedException, ExecutionException {
        int count = 100;
        CountDownLatch latch = new CountDownLatch(count);
        Stopwatch stopwatch = Stopwatch.createStarted();
        consumMsg.parallel(count);
        System.out.println("并行流耗时:" + stopwatch.elapsed(TimeUnit.MILLISECONDS) + "ms");

        System.out.println("==============");

        // 线程池反而多了几种状态,比如countDownLatch计数,实验结果比并行流还快
        Stopwatch watch = Stopwatch.createStarted();
        consumMsg.normalThread(count, 120, 100, latch);
        latch.await();
        System.out.println("线程池耗时:" + watch.elapsed(TimeUnit.MILLISECONDS) + "ms");
    }
}

3.2 提高并行流线程数

public class ConsumTest  extends BaseTest {

    @Autowired
    ConsumMsg consumMsg;

    @Test
    public void test() throws InterruptedException, ExecutionException {
        int count = 100;
        CountDownLatch latch = new CountDownLatch(count);

        // 指定并行流线程数
        System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "100");
        Stopwatch stopwatch = Stopwatch.createStarted();
        consumMsg.parallel(count);
        System.out.println("并行流耗时:" + stopwatch.elapsed(TimeUnit.MILLISECONDS) + "ms");

        System.out.println("==============");

        // 线程池反而多了几种状态,比如countDownLatch计数,实验结果比并行流还快
        Stopwatch watch = Stopwatch.createStarted();
        consumMsg.normalThread(count, 120, 100, latch);
        latch.await();
        System.out.println("线程池耗时:" + watch.elapsed(TimeUnit.MILLISECONDS) + "ms");
    }
}

3.3 测试结果:6w个任务

并行流适用场景-CPU密集型_第1张图片

4. 总结

​ 当并行流线程数保持默认时,线程方式消费任务效率高很多,但设置了并行流线程数后,并行流效率也提升了,只不过相比线程方式还是慢了1/3左右。

4.1 I/O密集型

​ 磁盘I/O、网络I/O都属于I/O操作,这部分操作是较少消耗CPU资源,一般并行流中不适用于I/O密集型的操作,就比如上诉描述的消息推送,涉及到了大量I/O,使用并行流反而慢了很多。

4.1 CPU密集型

​ 计算类型就属于CPU密集型了,这种操作并行流就能提高运行效率。

5. 参考资料

http://www.importnew.com/14742.html

https://www.itranslater.com/qa/details/2110153885791814656

《java 8 in action》第七章 并行数据处理与性能

你可能感兴趣的:(java)