记一次代码优化

一、前言

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

二、案例介绍与优化

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

//0
    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 {
                EXECUTOR_SERVICE.submit(() -> {
                    // 2.1模拟同步处理url,并返回结果
                    System.out.println(Thread.currentThread().getName() + " " + url);
                    Thread.sleep(30000);
                    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");
    }
  • 如上代码0创建了一个线程个数为8的线程池。

  • 代码1创建了一个图片列表

  • 代码2意为使用并行流把图片url下载任务投递到线程池EXECUTOR_SERVICE后,通过流的collect方法,把url和url处理结果收集起来变成map返回。

  • 代码2.1我们模拟同步根据url下载文件,并返回处理结果。

  • 代码3则使用main线程打印整个处理耗时和处理结果。

如上代码,运行起来确实可以实现并发下载图片功能,但是里面有个细节,就是默认并行流使用的是对于这段代码来说,首先使用了并行流,而并行流默认使用的是ForkJoinPool中的commonPool,而该commonPool是真个JVM内单实例的,如果commonPool内的线程全部阻塞了,则其他使用它的地方将转换为调用线程来执行。

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

为了验证上面说法,大家运行上面代码,然后可以自行jstack线程堆栈,会发现如下:

首先我们会看到3个EXECUTOR_SERVICE线程池中的线程在执行图片下载任务:


image.png

然后我们会看到有两个commonPool中的线程,在等待投递到EXECUTOR_SERVICE中的两个任务执行完毕:


image.png

并且可以看到main线程也在等待投递到EXECUTOR_SERVICE中的一个任务的执行结果:


image.png

所以这里为了等待投递到线程池EXECUTOR_SERVICE中的三个任务执行完毕,耗费了三个线程。其实可以把上面代码改造为如下:

    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 {
                        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");
    }
  • 如上代码2我们把图片下载任务投递到了线程池EXECUTOR_SERVICE后调用线程马上返回了对于的Future对象,然后我们通过流的collect方法把url和对应的future对象收集到了futureMap中,这个过程是异步的,不会阻塞调用线程。

  • 代码3 main线程则循环获取每个future的执行结果,并且通过流的collect方法把url和对应的future执行结果收集到map.

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

三、总结

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

更多技术分享,请扫描关注微信公众号:

file
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270