关于多线程计算结果同步

前言

日常搬砖,接到了将串行调用RPC服务(不要问我为啥串行 我也不明白)的业务逻辑优化的活儿,经过苦心思考,问了周边大神同学,醍醐灌顶,有了此文。

多线程获取计算结果方式

一想到多线程查询数据再汇总,我想到的是以下三种方式,根据业务及性能选择最优解。

Future同步式

  • Future同步式,想必大家都很清楚,就是get方法,源码注释如下。

Waits if necessary for the computation to complete, and then retrieves its result. 释义:Future.get()在线程计算结束前,一直处于等待状态。

代码

public static void main(String[] args) throws InterruptedException, ExecutionException {
        long l = System.currentTimeMillis();
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        Future<Integer> future = executorService.submit(() -> {
            System.out.println("执行耗时操作...");
            timeConsumeOp();
            // 耗时3000ms
            return 100;
        });

        Future<Integer> future1 = executorService.submit(() -> {
            System.out.println("执行耗时操作1...");
            // 耗时2000ms
            timeConsumeOp1();
            return 101;
        });

		// 依次将future、future1添加进列表
        List<Future<Integer>> futureList = Lists.newArrayList(future, future1);
        int s = 0;
        for (Future<Integer> future2 : futureList) {
	        // 线程计算结果
	        System.out.println("返回结果:" + future2.get());
            s = s + future2.get();
        }
        // 求和
        System.out.println("计算结果:" + s);
        // 主线程耗时
        System.out.println("主线程耗时:" + (System.currentTimeMillis() - l));
    }

    private static void timeConsumeOp() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void timeConsumeOp1() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
复制代码
  • 结果
执行耗时操作...
执行耗时操作1...
返回结果:100
返回结果:101
计算结果:201
主线程耗时:3077
复制代码

解析

  • 调用是同步的,从主线程耗时可以看出,主线程处于等待状态,等待计算结果返回才开始运行,因此耗时为3000ms左右。

  • 获取计算结果取决于调用顺序,尽管future1计算时间比future短(2000ms < 3000ms),应该是先得到101再得到100,但是Future.get()方法取决于调用顺序,因为遍历futureList时先调用的futureget方法。

  • 我们将futureList元素顺序换下,看看结果。

// 先future1
List<Future<Integer>> futureList = Lists.newArrayList(future1, future);
复制代码
  • 结果
执行耗时操作...
执行耗时操作1...
返回结果:101
返回结果:100
计算结果:201
主线程耗时:3076
复制代码

CompletionService

  • CompletionService使用excutor实现任务执行,添加了LinkedBlockingQueue将完成的FutureTask添加进来。
	// 使用线程数为2的线程池初始化
    private static CompletionService<Integer> completionService = new ExecutorCompletionService<> (Executors.newFixedThreadPool(2));
复制代码

代码

public static void main(String[] args) throws InterruptedException, ExecutionException {
        long l = System.currentTimeMillis();
        
        Future<Integer> future = completionService.submit(() -> {
            System.out.println("执行耗时操作...");
            timeConsumeOp();
            return 100;
        });

        Future<Integer> future1 = completionService.submit(() -> {
            System.out.println("执行耗时操作1...");
            timeConsumeOp1();
            return 101;
        });

        int s = 0;

        for (int i = 0; i < 2; i++) {
            int result = completionService.take().get();
            System.out.println("返回结果:" + result);
            s += result;
        }

        System.out.println("计算结果:" + s);
        System.out.println("主线程耗时:" + (System.currentTimeMillis() - l));
    }

    private static void timeConsumeOp() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void timeConsumeOp1() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
复制代码
  • 结果
执行耗时操作...
执行耗时操作1...
返回结果:101
返回结果:100
计算结果:201
主线程耗时:3066
复制代码

解析

  • 调用是同步的,从主线程耗时可以看出,主线程处于等待状态,等待计算结果返回才开始运行,因此耗时为3000ms左右。
  • 获取计算结果取决于计算完成顺序,尽管future1计算时间比future短(2000ms < 3000ms),因此得到101再得到100。

Future回调式 + CountDownLatch

Future回调式

  • 实现Future回调式的方式有多种,Guava就提供了两种方式,ListeningExecutorServiceCompletableFuture,也可以使用NettyFuture
  • 理解回调式,Android开发的经验就有用武之地了,其实就是异步调用,像Android里主线程负责渲染,访问网络的线程得到结果了回调触发主线程修改UI。
  • 本文使用CompletableFuture实现,有兴趣的可以使用其他方式。

CountDownLatch(CycleBarrier)

  • CountDownLatch类似于“计数器”,countdown()方法会将“计数器”减一,调用的await()的线程会在“计数器”为0之前,处于等待状态。

结果存储

  • 回调式的特点在于计算结果是在工作线程中返回的,因此要将结果同步下,可以使用concurrent集合,或者使用类似Map<AtomicInteger,Object>数据结果将各个子线程的结果汇总下。

代码

public static void main(String[] args) throws Exception{
        CountDownLatch countDownLatch = new CountDownLatch(2);

        List<Integer> list = Collections.synchronizedList(new ArrayList<>(2));
        long l = System.currentTimeMillis();
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("执行耗时操作...");
            timeConsumeOp();
            return 100;
        });
        CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(new Supplier<Integer>() {
            @Override
            public Integer get() {
                System.out.println("执行耗时操作1...");
                timeConsumeOp1();
                return 101;
            }
        });

        completableFuture.whenComplete((integer, throwable) -> {

            if (throwable != null) {
                System.out.println("运行错误");
            } else {
                System.out.println("计算结果:" + integer + " 线程名:" + Thread.currentThread().getName());
                list.add(integer);
            }
            countDownLatch.countDown();

        });

        completableFuture1.whenComplete((integer, throwable) -> {

            if (throwable != null) {
                System.out.println("运行错误");
            } else {
                System.out.println("计算结果:" + integer + " 线程名:" + Thread.currentThread().getName());
                list.add(integer);
            }
            countDownLatch.countDown();
        });


        System.out.println("计算结果汇总前 主线程还在运行:" + (System.currentTimeMillis() - l));
        // 主线程等待
        countDownLatch.await();

        int s = 0;
        for (int i : list) {
            s += i;
        }
        System.out.println("计算结果为:" + s + " 耗时 " + (System.currentTimeMillis() - l));
    }
复制代码
  • 结果
执行耗时操作...
执行耗时操作1...
计算结果汇总前 主线程还在运行:70
计算结果:101 线程名:ForkJoinPool.commonPool-worker-2
计算结果:100 线程名:ForkJoinPool.commonPool-worker-1
计算结果为:201 耗时 3072
复制代码

解析

  • 回调式,即异步调用不会使主线程处于等待状态。
  • countDownLatch.countDown()的时机要在计算结果存储之后,否则可能会漏掉线程运行结果。
  • countDownLatch.await()主线程会等待所有的线程countDown后,开始运行,可以看到耗时。
  • 使用了Collections.synchronizedList,原因为结果在不同线程返回,加锁保证计算结果存储。

结语

本文综合了几种笔者能想到的方案,并进行了简要分析,不见得都对,大家有更好的方案,可以与笔者交流。

参考文献

www.cnkirito.moe/future-and-…

猜你喜欢

转载自juejin.im/post/5d5556df6fb9a06acf2b5597