并发编程系列之如何正确使用线程池?

并发编程系列博客

原文链接

并发编程系列之如何正确使用线程池?在上一章节的学习中,我们掌握了线程的基本知识,接着本博客会继续学习多线程中的线程池知识

1、线程是不是越多越好?

在学习多线程之前,读者可能会有疑问?如果单线程跑得太慢,那么是否就能多创建多个线程来跑任务?并发的情况,线程是不是创建越多越好?这是一个很经典的问题,画图表示一下创建很多线程的情况,然后进行情况分析。


在这里插入图片描述
  • 创建线程和销毁线程都是需要时间的,如果创建时间+销毁时间>执行任务时间就很不划算
  • 创建后的线程是需要内存去存放的,创建的线程对应一个Thread对象,对象是会占用JVM的堆内存的,根据jvm规范,一个线程默认最大栈大小为1M,这个栈空间也是需要从系统内存中分配的,所以线程越多,需要的内存就越多
  • 创建线程,操作系统是需要频繁进行线程上下文切换的,所以线程创建太多,是会影响性能的

上下文切换(context switch):对于单核CPU来说,在一个时刻只能运行一个线程,对于并行来说,单核cpu也是可以支持多线程执行代码的,CPU是通过给线程分配时间片来解决的,所谓时间片是CPU给每个线程分配的时间,时间片的时间是非常短的,所以执行完成一个时间片后,进行任务切换,切换之前先保存这个任务的状态,以便于下次换回来的时候,可以加载这个任务的状态,所以从保存任务状态到再加载任务的过程称为上下文切换,不仅在线程间可以上下文切换,进程也同样可以

2、如何正确使用多线程?

  • 如果是计算型任务?
    CPU数量的1~2倍即可
  • 如果是IO密集型任务?
    就需要多一些线程,要根据具体的io阻塞时长来进行考量决定

3、Java线程池的工作原理

在这里插入图片描述
  • 接收任务,放入线程池的任务仓库
  • 工作线程从线程池的任务仓库取,执行
  • 没有任务时,线程阻塞,有任务时唤醒线程

4、掌握JUC线程池API

  • Executor : 接口类

  • ExecutorService:加入关闭方法和对Runnable、Callable、Future的支持


    在这里插入图片描述
    • shutdown:已经提交的会执行完成
    • shutdownNow:正在执行的会执行完成,未来执行的返回
    • awaitTermination:阻塞等待任务关闭完成
    • submit类型的:都是提交任务的,支持Runnable和Callable
    • invokeAll类型的:执行集合中所有任务
  • ScheduleExecutorService :加入对定时任务的支持
    在这里插入图片描述

    其中schedule(Runablle , long, Timeunit)schedule(Callable<V> , long, TimeUnit)表示的是多久后执行,而scheduleAtFixedRate方法和scheduleWithFixedDelay方法表示的都是周期性重复执行的

再描述scheduleAtFixedRate方法和scheduleWithFixedDelay方法的区别:
scheduleAtFixedRate:以固定的时间频率重复执行任务,如每10s ,也就是两个任务直接以固定的时间间隔执行,不管任务执行完成与否

在这里插入图片描述

scheduleWithFixedDelay:以固定的任务时延迟来重复执行任务,这种任务不管任务执行多久都执行完成,然后隔预定的如3s,接着执行下一个任务,每个任务之间的间隔都是一样的

在这里插入图片描述
  • Executors:快速得到线程池的工具类,创建线程池的工厂类
    • newFixedThreadPool(int nThreads):创建一个固定大小、任务队列 无界的线程池。线程池的核心线程数=最大线程池=nThreads
    • newCachedThreadPool():创建的是一个大小无界的缓冲线程池。它的任务队列是一个同步队列。如果队列中有空闲的线程,则用空闲线程执行,如果没有就创建新线程执行。池中线程空闲超过60s,就会被释放。缓冲线程池使用于执行耗时比较小的异步任务。线程池的核心线程数=0,最大线程池=Integer.MAX_VALUE
    • newSingleThreadExecutor():创建的是只有一个线程来执行无界任务队列的单一线程池。该线程池按顺序执行一个一个加入的任务,任何时刻都只有一个线程在执行。单一线程池和newFixedThreadPool(1)的区别在于,单一线程池的池大小是不能再改变的
    • newScheduleThreadPool(int corePoolSize): 能定时执行任务的线程池,该池的核心线程数由参数corePoolSize指定,最大线程数=Integer.MAX_VALUE
    • newWorkStealingPool():以当前系统可用的处理器数作为并行级别创建的work-stealing thread pool(ForkJoinPool)
    • newWorkStealingPool(int parallelism):以指定的parallelism并行级别创建的work-stealing thread pool(ForkJoinPool)
  • ThreadPoolExecutor:线程池的标准实现


    在这里插入图片描述

    下面列举出ThreadPoolExecutor的主要参数:

参数 描述
corePoolSize 核心线程数量
maxPoolSize 最大线程数量
keepAliveTime+时间单位 空闲线程的存活时间
ThreadFactory 线程工厂,用于创建线程
workQueue 用于存放任务的队列,可以称之为工作队列
Handler 用于处理被拒绝的任务

虽然Executors使用起来很方便,不过在阿里编程规范里是强调了慎用Executors创建线程池,下面摘录自阿里编程规范手册:

【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,
这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

ThreadPoolExecutor的基本参数:

new ThreadPoolExecutor(
                2, // 核心线程数 
                5, // 最大线程数
                60L, // keepAliveTime,线程空闲超过这个数,就会被销毁释放
                TimeUnit.SECONDS, // keepAliveTime的时间单位
                new ArrayBlockingQueue(5)); // 传入边界为5的工作队列

画流程图表示,线程池的核心参数是corePoolSize、maxPoolSize、workQueue(工作队列)


在这里插入图片描述

线程池工作原理示意图:任务可以一直放,直到线程池满了的情况,才会拒绝,然后除了核心线程,其它的线程会被合理回收。所以正常情况下,线程池中的线程数量会处在 corePoolSize 与 maximumPoolSize 的闭区间内


在这里插入图片描述

ThreadPoolExecutor基本实例:
ExecutorService service = new ThreadPoolExecutor(2, 5,
                60L, TimeUnit.SECONDS,
                new ArrayBlockingQueue(5));
 service.execute(() ->{
     System.out.println(String.format("thread name:%s",Thread.currentThread().getName()));
 });
  // 避免内存泄露,记得关闭线程池
 service.shutdown();

ThreadPoolExecutor加上Callable、Future使用的例子:

public static void  main(String[] args) {
    ExecutorService service = new ThreadPoolExecutor(2, 5,
                    60L, TimeUnit.SECONDS,
                    new ArrayBlockingQueue(5));
    
    Future<Integer> future = service.submit(new CallableTask());
    Thread.sleep(3000);
    System.out.println("future is done?" + future.isDone());
    if (future.isDone()) {
        System.out.println("callableTask返回参数:"+future.get());
    }
    service.shutdown();
}

static class CallableTask implements Callable<Integer>{
     @Override
     public Integer call() {
         return ThreadLocalRandom.current().ints(0, (99 + 1)).limit(1).findFirst().getAsInt();
     }
 }

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

推荐阅读更多精彩内容