Java线程池初探

Java线程池初探

1. 为什么要用线程池?


多核CPU时代,为更好利用资源以获取更高的性能,多线程编程早已普通应用。手工创建和销毁线程有以下弊端。

  • 线程上下文的切换需要JVM和操作系统的参与,若频繁操作势必造成CPU和内存资源的浪费。
    举个栗子。若为每个请求都创建线程来处理,当有大量恶意请求到来时,内存轻意就被攻占了。此时你是否会向运维同学央求...
  • 程序员需要处理线程的异常状态,如线程因为出错导致异常等。否则,不知哪天用户就会微笑着对你说...
  • 良好的软件设计不建议手工创建和销毁线程。

幸运的是,线程池可以提供一条龙服务,帮助我们管理线程的创建、执行和销毁。还能根据系统承受能力,合理利用已有线程,减轻负担。一名话概括:集中管理线程,以期达到收益最大化,风险最小化

2. 线程池如何使用?


2.1 类结构
  • 线程池从JDK1.5版本开始引入,在JDK1.7中线程池框架的核心类是ThreadPoolExecutor,其关键类结构如下图:
类结构
2.2 ThreadPoolExecutor构造方法
  • JDK源码中该类的构造方法有4个,但最终都是调用以下构造方法来创建一个线程池。

    ThreadPoolExecutor (
        int corePoolSize,
        int maximumPoolSize,
        long keepAliveTime,
        TimeUnit unit,
        BlockingQueue<Runnable> workQueue,
        ThreadFactory threadFactory,
        RejectedExecutionHandler handler
    )
    
    • corePoolSize:核心线程数量。达到该数量后新提交的线程会进入任务队列中排队。
    • maximumPoolSize:池中允许的最大线程数量。需要与任务队列的类型结合使用。
    • keepAliveTime:池中线程数量超过corePoolSize时,空闲线程会在多长时间内被回收。
    • unitkeepAliveTime的时间单位。
    • workQueue:任务队列。它负责暂时保存未被执行的任务。
    • threadFactory:创建线程的工厂。
    • handler:拒绝策略,即当提交到池中的线程数量超过其最大容量时,如何拒收。
  • 看了上面这一本正经的定义,是不是有点懵圈?! 下面用一个春运购票的栗子来说明下。

    • 假定春运期间你想要购买K6124次列车。通常情况下该列车有座位1000个(corePoolSize)。
    • 但春运期间车票异常抢手,初始的1000张票秒光,可怜的你没有抢到。不过12306推出一项“高端服务”:候补。即没买到票的人可以进入一个队列等待,这个队列就是workQueue。你选择候补,随即进入该队列等待。考虑到实际情况队列不应过大,假定其大小设为300。
    • 随着候补的人起来越多,没多久候补队列也满了。12306充分考虑到了大家回家的迫切需求,决定多加2个车厢来提供额外的200张票。这样就能提总计1200张票maximumPoolSize
    • 没过多久,额外提供的票也全部卖出。此时候补列队仍然是满的,售票系统则不再接受新的候补请求(拒绝策略handler)。
    • 春运结束后票也不太紧张了。若连续5天该车卖出的票数不超过1000张,则当初额外追加的2个车厢就被收回了。这里的5天就相当于keepAliveTime,天就是它的单位unit
2.3 线程池工厂Executors

如果不想用上面ThreadPoolExecutor看似复杂的构造方法去创建线程池,可以使用JDK提供的线程池工厂Executors。它提供了几种常用线程池的创建方法,主要有以下几种。

  • newFixedThreadPool(n): 创建固定长度的线程池,任务队列大小为Integer.MAX_VALUE,即相当于是无界队列。
  • newSingleThreadExecutor: 创建只有一个线程的线程池,任务队列大小也为Integer.MAX_VALUE
  • newScheduledThreadPool: 创建一个支持定时及周期性任务执行的线程池。它的任务队列采用数组来存储元素,其初始大小为16,但会动态扩容。其maximumPoolSizeInteger.MAX_VALUE
  • newCachedThreadPool: 创建一个可根据实际情况动态调整线程数量的线程池,任务处理采用不排队直接提交的方式。其maximumPoolSizeInteger.MAX_VALUE,空闲线程在60秒内会被自动回收。

3. 线程池内部的原理如何?


3.1 工作流程

当主线程向线程池提交新任务时,工作流程概括如下所示:

工作流程
  1. 若池中线程数量小于corePoolSize,则创建新的线程来执行此新添加的任务。
  2. 若池中线程数量大于等于corePoolSizeworkQueue还有空间,则将该任务放入队列。
  3. 若池中线程数量大于等于corePoolSizeworkQueue已满,但池中线程数量小于maximumPoolSize,则会创建新的线程来执行此新添加的任务。
  4. 若池中线程数量达到了maximumPoolSize,则不再授受新任务,并调用拒绝策略来处理。
3.2 任务队列

线程池使用阻塞式BlockingQueue来缓存暂时未处理的任务,以生产-消费者模式进行任务的存取,目的是在保证安全并发的前提下提高效率。BlockingQueue是一种特殊的集合,其主要类图如下:

任务队列

存取数据操作有3组

  • add/remove: 添加未能成功抛出异常。
  • offer/poll: 添加或获取未成功时即刻返回
    此外还提供带超时时间的版本,在指定时间内有限阻塞,若超时仍未成功则返回。
  • put/take: 添加或获取未成功时,会无限阻塞,直到条件满足。

几种阻塞式队列

  • ArrayBlockingQueue:内部结构基于数组,生产端和消费端共用一个锁,创建时必须指明队列长度。
  • LinkedBlockingQueue:内部基于链表,生产端和消费端使用各自独立的锁控制存取。创建时若未指明长度,则默认为Integer.MAX_VALUE。线程池工厂中的newFixedThreadPoolnewSingleThreadExecutor就是使用的该队列。
  • SynchronousQueue: 无缓冲队列,内部只能容纳一个元素。添加元素时后会阻塞,直到元素被取走后才能继续添加。newCachedThreadPool使用该队列。
3.3 拒绝策略

当线程池不能再授受新任务的提交时,会采用以下几种策略进行处理。

拒绝策略
  • AbortPolicy: 丢弃任务并抛出异常,这也是JDK线程池中的默认策略。
  • CallerRunsPolicy: 交由调用线程处理该任务。
  • DiscardOldestPolicy: 丢弃队列头部的任务,重新提交被拒绝的任务。
  • DiscardPolity: 丢弃任务但不抛出异常,相当于什么也没发生。

4. 灵魂拷问?


  • 并发情况下的共享资源访问

多线程应用程序的复杂性会有所提升,尤其是涉及到共享资源的访问时,如若处理不当则系统出BUG的概率将上升。竞态下的共享资源访问需要用到锁机制来确保程序运行的正确性,这又是一个可以探讨的话题。

  • 使用线程池工厂的注意事项

JDK中线程池工厂可以帮助我们快速创建线程池,使用虽然很便利,但我们要清楚地知道每种线程池背后的原理。如newFixedThreadPool,其线程池容量是有限的,但其任务队列却是无界的。如果提交的任务过多,可能会造成任务列队快速增长,最终导致内存溢出。

又如newCachedThreadPool,其使用的是无缓冲的任务队列,但其maximumPoolSize却是Integer.MAX_VALUE(相当于是无限的)。如果任务处理的速度远远落后于任务提交的速度,同样可能会因为创建过多线程而导致内存溢出。

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