ThreadPoolExecutor解析

为什么要使用线程池?

引用自 http://ifeve.com/java-threadpool/ 的说明:

1.频繁的创建和销毁线程会影响系统的性能,线程池可以通过重复利用已创建的线程来降低线程创建和销毁造成的资源消耗;

2.提高响应的速度。当任务到达时,任务可以不需要等到线程的创建就能立刻执行任务。

3.提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,管理与监控。

线程池的类图:

线程池的类图

ThreadPoolExecutor的构造方法:

1.

构造方法

corePoolSize:核心线程数量,当有新的任务提交时,会在线程池创建一个新的线程,直到线程数量达到核心线程数量为止。

maximumPoolSize:线程池允许的最大线程数量。

keepAliveTime:当线程池中线程的数量大于corePoolSize的时候,没有新的任务提交的话,超过corePoolSize所允许的等待时间。

unit:时间的单位。可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS)和毫微秒(NANOSECONDS)。

workQueue:阻塞队列。当线程池中的线程数量已经达到corePoolSize之后,再通过execute添加的新的任务会被加到workQueue队列中。

threadFactory:创建新线程的工厂。

handler:当线程池和队列都满了的时候,执行的一种策略,用来处理新提交的任务。有以下4种策略:

1.AbortPolicy:直接抛出异常(默认);

2.CallerRunsPolicy:使用调用者所在的线程来执行任务;

3.DiscardOldestPolicy:丢弃阻塞队列中考前的任务,并执行当前的任务;

4.DiscardPolicy:直接丢弃任务;

SynchronousQueue:直接提交队列,该队列没有容量,每一个插入操作都要等待一个相应的删除操作,反之,每一个删除操作都要等待对应的插入操作。所以他不保存任务,总是将任务提交给线程执行,如果没有空闲的线程,则创建新的线程,当线程数量达到最大,则执行拒绝策略。

ArrayBlockingQueue:有界任务队列,线程池的线程数小于corePoolSize,则创建新的线程,大于corePoolSize,则将新的任务加入等待队列。若等待队列已满,则在总线程不大于maximumPoolSize下,创建新的线程执行任务,大于maximumPoolSize则执行拒绝策略。

LinkedBlockingQueue:无界队列,除非系统资源耗尽,否则不存在任务入队失败的情况。线程池的线程数小于corePoolSize,则创建新的线程,大于corePoolSize,则将新的任务加入等待队列。

PriorityBlockingQueue:优先任务队列,可以控制任务的执行先后顺序,是无界队列。ArrayBlockingQueue,LinkedBlockingQueue都是按照先进先出算法处理任务的,PriorityBlockingQueue可以根据任务自身的优先顺序先后执行。

线程池的工作流程

线程池的工作流程

从上图可以看出,当一个新的任务提交到线程池的时候,线程池的处理流程如下:

1.首先判断核心线程池是否已满?如果没满,则创建一个新的线程来执行任务,满了进入下一个流程;

2.判断队列是否已满?如果没满,则将新提交的任务存储在工作队列里,满了进入下一个流程;

3.最后判断整个线程池是否已满?如果没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略处理此任务。

源码分析:

上边的过程作了一个大概的了解,下边看一下源代码的实现:

execute()

1.workerCountOf(c) < corePoolSize

通过workerCountOf()方法拿到当前活动的线程数,如果当前活动的线程数小于corePoolSize,则新建一个线程放入到线程池中,并且把任务添加到该线程。

2. if(addWorker(command, true))

PS:addWorker中的第二个参数表示添加线程的数量是根据corePoolSize来判断还是maximumPoolSize来判断;如果为true,根据corePoolSize来判断;如果为false,则根据maximumuPoolSize来判断。

3.if(isRunning(c) && workQueue.offer(command))

如果当前线程是运行状态并且任务添加到队列成功

4.if (! isRunning(recheck) && remove(command))

这里是一个二次检查,因为入队之后状态还是可能发生变化的。

5.else if (workerCountOf(recheck) ==0)          addWorker(null, false);

获取线程池中的有效线程数,如果等于0,则执行addWorker()方法。

传入的参数表示:

第一个参数null, 表示在线程池中创建一个线程,但是不启动;

第二个参数为false,将线程池的上限设置为maximumPoolSize,添加线程时根据maximumPoolSize来判断;

6.else if (!addWorker(command, false))       reject(command);

如果队列失败了,尝试添加新的任务,如果失败则拒绝该任务。

总结:简单来说在执行execute()方法时如果状态一直是RUNNING时的执行过程如下:

1.如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务;

2.如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中;

3.如果workerCount >= corePoolSize && workerCount < maximumPoolSize, 且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务;

4.如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务(默认处理方式直接抛出异常);

PS:这里addWorker(null, false); 也就是创建一个线程,但是并没有传入任务,因为任务已经被添加到workQueue中了,所以worker在执行的时候,会直接从workQueue中获取任务。因此当workerCountOf(recheck) == 0的时候执行addWorker(null,false)也是为了保证线程池在RUNNING状态下必须要有一个线程来执行任务。

runWorker()

简单说明一下runWorker方法的执行过程:

1.while循环不断的通过getTask()方法获取任务;

2.然后运行任何任务之前,拿到锁来防止执行任务时中断;

3.然后确保除非线程池停止,那么要保证当前线程是中断状态,否则保证当前线程不是中断状态;

4.task.run()执行任务;

5.当task为null的时候则跳出循环,执行processWorkerExit()方法;

6.runWorker方法执行完毕,也代表着Worker中的run方法执行完毕,销毁线程。

PS:beforeExecute方法和afterExecute方法在ThreadPoolExecutor类中是空的,留给子类实现。

completedAbruptly变量用来表示执行任务的过程中是否出现了异常,processWorkerExit方法会对这个变量值进行判断。

线程池的合理配置

线程池需要合理的配置,首先应该先分析任务的特点,比如:

1.任务的性质:CPU密集型任务,IO密集型任务还是混合型任务;

2.任务的优先级:高,中,低。

3.任务的执行时间:长,中,短。

IO密集型:由于需要等待IO操作,线程并不是一直在执行任务,可以 配置多一点线程。

CPU密集型:CPU密集型任务需要大量的计算,并且没有什么阻塞,CPU一直全速运行,所以尽可能少的配置线程数量。

混合型:混合型任务如果可以拆分,则拆分成CPU密集型和IO密集型任务,只要两个任务执行的时间相差不是特别大,拆分后执行的吞吐率应该高于串行执行的吞吐率。反之如果任务执行时间相差太大,就没必要拆分。

优先级:优先级不同的任务可以使用优先级队列PriorityBlockQueue来处理,它可以让优先级高的任务先执行(如果一直有优先级高的任务提交到队列里,可能会造成优先级低的任务永远得不到执行)。

执行时间:执行时间不同的任务也可以使用优先级队列,让执行时间短的任务先执行。

 线程池的监控

线程池提供了一些属性可以进行监控,这样有利于我们更好的使用线程池。

taskCount:线程池需要执行的任务数量;

completedTaskCount:线程池在运行过程中已经完成的任务数量。

largestPoolSize:线程池曾经创建过的最大线程数量。通过这个我们可以知道线程池是否满过。如果等于线程池的最大大小,则表示线程池曾经满过。

getPoolSize:线程池的数量。

getActiveCount:活动的线程数。

通过继承线程池并且重写线程池的beforeExecute, afterExecute 和terminated方法,就可以在任务执行前,执行后和线程池关闭前做一些事情。比如监控任务的最大执行时间,最小执行时间等。这几个方法在线程池里都是空方法。

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

推荐阅读更多精彩内容