多线程代码优化

前言

并发运行相比串行执行很好,因为其可以减少执行时间,但是并发用的不对,也会造成资源浪费,本文我们就来探究一例子。

二、案例介绍与优化

有这样一段代码,根据传递的url列表,并发的去下载url对于的文件内容,原来代码模拟如下:

package com.zyy.java.thread.problem;

import com.alibaba.fastjson.JSON;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author zhaoyy
 * @version 1.0
 * @description TODO
 * @date 2021/10/11
 **/
public class ThreadPoolTest01 {
    private final static ThreadPoolExecutor EXECUTOR_SERVICE = new ThreadPoolExecutor(8, 8, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(1));

    public static void main(String[] args) {

        // 1.创建图片列表
        List<String> imageList = new ArrayList<String>();
        for (int i = 0; i < 3; ++i) {
            imageList.add(i + "");
        }

        long start = System.currentTimeMillis();

        // 2.并发处理url
        Map<String, String> resultMap = imageList.parallelStream().collect(Collectors.toMap(url -> url, url -> {
            try {
              return EXECUTOR_SERVICE.submit(() -> {
                    // 2.1模拟同步处理url,并返回结果
                    System.out.println(Thread.currentThread().getName() + " " + url);
                    Thread.sleep(300000);
                    return "jiaduo" + url;
                }).get();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return "";
        }));

        // 3.打印结果
        long costs = System.currentTimeMillis() - start;
        System.out.println("result:" + costs + " " + JSON.toJSONString(resultMap));
        System.out.println("main is over");
    }

}

1.如上代码0创建了一个线程个数为8的线程池。
2.代码1创建了一个图片列表
3.代码2意为使用并行流把图片url下载任务投递到线程池4.EXECUTOR_SERVICE后,通过流的collect方法,把url和url处理结果收集起来变成map返回。
5.代码2.1我们模拟同步根据url下载文件,并返回处理结果。
6.代码3则使用main线程打印整个处理耗时和处理结果。
如上代码,运行起来确实可以实现并发下载图片功能,但是里面有个细节,就是默认并行流使用的是对于这段代码来说,首先使用了并行流,而并行流默认使用的是ForkJoinPool中的commonPool,而该commonPool是真个JVM内单实例的,如果commonPool内的线程全部阻塞了,则其他使用它的地方将转换为调用线程来执行。

另外这里会导致多个commonPool中的线程处于阻塞状态等待异步任务执行完毕。这里假设imageList中有3个URL,则我们会有3个线程(3个commonpool中的线程)分别把下载图片任务投递到executorService,然后这3个线程各自调用了返回的future的get系列方法等待上传任务的完成,所以这会导致commonPool内的3个线程和调用线程被阻塞。

为了验证上面说法,大家运行上面代码,然后可以通过jvisualvm,会发现如下:

首先我们会看到3个EXECUTOR_SERVICE线程池中的线程在执行图片下载任务:
在这里插入图片描述
然后我们会看到有三个个commonPool中的线程,在等待投递到EXECUTOR_SERVICE中的三个任务执行完毕,并且可以看到main线程也在等待投递到EXECUTOR_SERVICE中的一个任务的执行结果,所以这里为了等待投递到线程池EXECUTOR_SERVICE中的三个任务执行完毕,耗费了三个线程。其实可以把上面代码改造为如下:

package com.zyy.java.thread.problem;

import com.alibaba.fastjson.JSON;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author zhaoyy
 * @version 1.0
 * @description TODO
 * @date 2021/10/11
 **/
public class ThreadPoolTest02 {
    private final static ThreadPoolExecutor EXECUTOR_SERVICE = new ThreadPoolExecutor(8, 8, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(1));

    public static void main(String[] args) {

        // 1.创建图片列表
        List<String> imageList = new ArrayList<String>();
        for (int i = 0; i < 3; ++i) {
            imageList.add(i + "");
        }

        long start = System.currentTimeMillis();

        // 2.并发执行,并保持url对应的future
        Map<String, Future<String>> futureMap = imageList.stream().collect(Collectors.toMap(url -> url, url -> {
            return EXECUTOR_SERVICE.submit(() -> {
                // 2.1模拟rpc同步处理url,并返回结果
                System.out.println(Thread.currentThread().getName() + " " + url);
                Thread.sleep(300000);
                return "jiaduo" + url;
            });
        }));

        // 3.调用线程同步等待所有任务执行完毕
        Map<String, String> resultMap = futureMap.entrySet().stream()
                .collect(Collectors.toMap(entry -> entry.getKey(), entry -> {
                    try {
                       return entry.getValue().get();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    return "";
                }));

        // 3.打印结果
        long costs = System.currentTimeMillis() - start;
        System.out.println("result:" + costs + " " + JSON.toJSONString(resultMap));
        System.out.println("main is over");
    }

}

修改我们来看下线程分部情况:
在这里插入图片描述
1.如上代码2我们把图片下载任务投递到了线程池EXECUTOR_SERVICE后调用线程马上返回了对于的Future对象,然后我们通过流的collect方法把url和对应的future对象收集到了futureMap中,这个过程是异步的,不会阻塞调用线程。
2.代码3 main线程则循环获取每个future的执行结果,并且通过流的collect方法把url和对应的future执行结果收集到map.

运行上面代码,我们会发现除了有EXECUTOR_SERVICE中的三个线程在执行文件下载任务外,只有一个main线程在等待全部任务执行完毕,相比原来方法节省了2个commonPool里面的线程。

三、总结

并发固然好,但是用不对则会起到副作用,本例中原来方法如果url列表很大,则会导致commonpool里面的线程打满,则当前jvm内其它使用commonpool的地方则会自动转换为调用线程来执行了,会起不到预设的效果。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菜鸟-要努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值