Java并发学习笔记——第九章 Java中的线程池

Java并发学习笔记——第九章 Java中的线程池

Java中的线程池运用是并发开发中的总重之中。

Java中线程池时运用场景最多的并发框架,几乎所有异步或并发执行任务的程序都可以使用线程池。合理使用线程池可以带来三点好处:

  • 降低资源消耗:重复利用线程可降低因创建、销毁线程带来的损耗。
  • 提高响应速度:任务到达时不需要等待线程创建就能执行。
  • 提高线程的可管理性:线程是稀缺资源,若无限制地创建,会消耗系统资源、降低系统稳定性。使用线程池可以进行统一分配、调优和监控。

线程池实现原理

当一个新任务提交到线程池时,线程池处理流程如下:

  1. 判断核心线程池的线程是否都在执行任务。若不是,则创建一个新的工作线程来执行任务;否则进入下个流程。
  2. 判断工作队列是否已满。若没满,则将新提交的任务存储在这个工作队列中;否则进入下个流程。
  3. 判断线程池的线程是否都处于工作状态。若不是,则创建一个新的工作线程来执行任务;否则交给饱和策略处理这个任务。
ThreadPoolExecutor执行示意图

如图,执行ThreadPoolExecutor.execute()会产生四种情况:

  1. 若当前运行的线程少于corePoolSize,则创建新线程来执行任务(执行创建新线程需要获取全局锁)。即使此时有线程空闲。
  2. 若运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue(工作队列)。
  3. 若无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(执行创建新线程需要获取全局锁)。
  4. 若创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

ThreadPoolExecutor设计成上面的思路,主要是为了避免获取全局锁。在ThreadPoolExecutor完成预热后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2 。

工作线程:线程池创建线程时,将线程封装为工作线程WorkerWorker在执行完任务后不会销毁,会循环从BlockingQueue获取任务执行。

饱和策略:默认使用AbortPolicy

  • AbortPolicy:中止策略。抛出RejectedExecutionException,调用者可以捕获该异常。
  • DiscardPolicy:抛弃策略。当新提交的任务无法保存到队列中等待执行时,该策略会悄悄抛弃该任务。
  • DiscardOldestPolicy:抛弃旧的策略。将抛弃第一个在队列中等待的任务。
  • CallerRunsPolicy:调用者运行策略。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者(调用线程池执行任务的主线程),从而降低新任务的流程。它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。当线程池的所有线程都被占用,并且工作队列被填满后,下一个任务会在调用execute时在主线程中执行(调用线程池执行任务的主线程)。由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得工作者线程有时间来处理完正在执行的任务。在这期间,主线程不会调用accept,因此到达的请求将被保存在TCP层的队列中。如果持续过载,那么TCP层将最终发现它的请求队列被填满,因此同样会开始抛弃请求。当服务器过载后,这种过载情况会逐渐向外蔓延开来——从线程池到工作队列到应用程序再到TCP层,最终达到客户端,导致服务器在高负载下实现一种平缓的性能降低。

线程池使用

创建线程池

new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);

创建线程池需要输入几个参数:

  • corePoolSize(线程池基本大小):当一个任务提交到线程池,若线程池的线程数量没有达到corePoolSize,即使有空闲的基本线程,也会创建一个新线程来执行任务(为了尽快预热线程池)。如果调用prestartAllCoreThreads()方法则线程池会提前创建并启动所有线程。
  • maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。若工作队列满了,且线程数没有达到maximumPoolSize,则线程池会创建新的线程执行任务。
  • keepAliveTime(线程活动保持时间):工作线程空闲后保持存活的时间。若任务很多且每个任务执行时间短,可以调大该值提高线程利用率。若设置了ThreadPoolExecutor.allowCoreThreadTimeOut(false),则会维持corePoolSize个线程不回收。
  • unit (线程活动保持时间的单位):可选单位有天、小时、分钟、毫秒、微妙、纳秒。
  • workQueue(工作队列):在线程池预热后,新任务提交进线程池会被放进工作队列,任务调度时再从中取出来放入工作线程,提供了四种工作队列:
    • ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序。
    • LinkedBlockingQueue:基于链表的无界阻塞队列(最大容量为Integer.MAX),按照FIFO排序。因为是无界阻塞队列,所以maximumPoolSize不起作用。
    • SynchronousQueue:不缓存任务的阻塞队列,生产者放入一个任务必须等消费者取出这个任务。因此任务进入该队列后会直接进行调度处理,若没有可用工作线程,则直接创建新线程。
    • PriorityBlockingQueue:具有优先级的无界阻塞队列,优先级由参数Comparator实现。
  • threadFactory:用于设置创建线程的工厂,可以通过线程工厂为每个创建出来的线程设置更有意义的名字。开源框架的guava提供的ThreadFactoryBuilder可以快速给线程池里的线程设置有意义的名字。
  • handler(饱和策略):上文已概述过。

向线程池提交任务

有两种向线程池提交任务的方法,execute()submit()

  • execute()方法用于提交不需要返回值的任务,因此无法判断任务是否被执行成功。

    • threadPool.execute(new Runnable() {
          @Override
          public void run() {
              //TODO Auto-generated method stub
          }
      })
      
  • submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,可以通过该对象判断任务是否执行成功,且调用future.get()获取返回值。get()会阻塞当前线程直到任务完成,使用get(long timeout, TimeUnit unit)可以设定超时,若任务没执行完也依然返回(但不中断任务)。

关闭线程池

有两种关闭线程池的方法,他们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt()中断线程,所以无法响应中断的任务可能永远无法中止。

  • shutdown():将线程池状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
  • shutdownNow():先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程。

因此根据线程池特性,一般使用shutdown(),若任务不一定要执行完,可以用shutdownNow()

合理配置线程池

配置线程池需要分析的角度:

  • 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
  • 任务的优先级:高、中、低。
  • 任务的执行时间:长、中、短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。
  1. 性质不同的任务可以用不同规模的线程池分开处理。
    1. CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。
    2. IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu 。
    3. 混合型任务可以拆分为CPU密集型任务和IO密集型任务,分解后吞吐量将高于串行。若两个任务执行时间相差太大,则没有必要。
  2. 优先级不同的任务可以使用优先级队列PriorityBlockingQueue处理。
  3. 执行时间不同的任务可以交给不同规模的线程池处理,也可以使用优先级队列。
  4. 依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待时间越长,则CPU空闲时间越长,因此线程数应该设置得越大,才能更好利用cpu。
  5. 建议使用有界队列。有界队列能增加系统得稳定性和预警能力。如sql任务堆积的越多,队列会越长,会导致sql非常缓慢。

监控线程池

可以使用一些属性监控线程池:

  • taskCount:线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount
  • largestPoolSize:线程池里曾经创建过的最大线程数量。该数据可以判断线程池是否曾经满过。
  • getPoolSize:线程池线程数量。
  • getActiveCount:活动的线程数。

可以重写线程池的beforeExecuteafterExecuteterminated方法,在任务执行前、执行后和线程池关闭前执行一些代码进行监控。这些方法在线程池里是空方法。

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

推荐阅读更多精彩内容