Java并发编程初识-线程池
Posted 敲代码的程序狗
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程初识-线程池相关的知识,希望对你有一定的参考价值。
0. 前言
Java 中线程池是运用场景最多的并发框架,合理使用线程池可以带来诸多好处:
- 降低资源消耗:重复利用已创建的线程,降低了线程创建和销毁时造成的资源消耗
- 提高响应速度:任务到达时,不需要等待线程创建即可立即执行
- 提高线程的可管理性:线程的无限创建不仅消耗系统资源,还会降低系统的稳定性,可以通过线程池统一分配、调优和监控
1. 线程池的实现原理
1.1 线程池处理任务
向线程池提交一个任务时,主要处理流程:
- Step1:线程池判断核心线程池是否已满。如果没满则创建新线程来执行任务,如果满了则进入下个流程
- Step2:线程池判断任务队列是否已满。如果没满则将任务添加进任务队列,如果满了则进入下个流程
- Step3:线程池判断线程池是否已满。如果没满则创建新线程来执行任务,如果满了则交给拒绝策略处理
1.2 execute 方法的执行
ThreadPoolExecutor 执行 execute()
方法时,主要处理流程:
- Step1:如果运行的线程数小于 corePoolSize,则创建新线程来执行任务(需要获取全局锁)
- Step2:如果运行的线程数大于等于 corePoolSize,则将任务添加进 BlockingQueue
- Step3:如果无法将任务添加进 BlockingQueue(队列已满),则创建新线程来执行任务(需要获取全局锁)
- Step4:如果创建新线程将使当前运行的线程数超过 maximumPoolSize ,任务将被拒绝,并调用 RejectedExecutionHandler 的
rejectedExecution()
方法
ThreadPoolExecutor 采用上述步骤处理任务,是为了尽可能避免获取全局锁(那将会是一个严重的可伸缩瓶颈)
1.3 Worker 工作线程
线程池创建线程时,会将线程封装成工作线程 Worker ,工作线程执行完初始任务后,会从任务队列循环获取任务执行
工作线程执行任务的两种情况:
- 在
execute()
方法创建线程时,会让该线程执行当前任务 - 线程执行完步骤1的初始任务后,会循环从 BlockingQueue 中获取任务执行
2. 线程池的使用
2.1 线程池的创建
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
复制代码
-
corePoolSize:核心线程数
-
maximumPoolSize:最大线程数,如果线程池使用了无界的任务队列,该参数失效
-
keepAliveTime:线程存活时间,工作线程空闲(任务队列无任务可获取)时,保持存活的时间
-
unit:线程存活时间的单位
-
workQueue:任务队列,保存等待执行的任务的阻塞队列
-
ArrayBlockingQueue:基于数组结构的有界阻塞队列,按 FIFO 原则排序元素
-
LinkedBlockingQueue:基于链表的无界阻塞队列,按 FIFO 原则排序元素。吞吐量高于 ArrayBlockingQueue 队列,
Executors.newFixedThreadPool()
方法使用了该队列 -
SynchronousQueue:不存储元素的队列,每次插入操作必须等到另一个线程移除操作,否则插入操作一直阻塞。吞吐量高于 LinkedBlockingQueue 队列,
Executors.newSingleThreadExecutor()
方法使用了该队列 -
PriorityBlockingQueue:具有优先级的无界阻塞队列
-
-
threadFactory:创建线程的工厂
-
handler:拒绝策略,当工作线程和任务队列都满时需要采取一种策略处理提交的任务
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:只用调用者所在线程来执行任务
- DiscardOldestPolicy:丢弃任务队列最近一个任务,然后调用
execute()
方法处理任务 - DiscardPolicy:不处理,丢弃任务
2.2 向线程池提交任务
execute()
方法:提交不需要返回值的任务submit()
方法:提交需要返回值的任务,可以通过下列方法获取返回值Future.get()
方法:阻塞调用者所在线程直到任务执行完成返回结果Future.get(long timeout, TimeUnit unit)
方法:阻塞调用者所在线程,超过指定时间立即返回,任务可能未执行完成
2.3 关闭线程池
shutdown()
方法:将线程池的状态设置为 SHUTDOWN ,尝试中断空闲的线程shutdownNow()
方法:将线程池的状态设置为 STOP ,尝试停止所有线程(执行任务中、空闲)
2.4 合理的配置线程池
合理配置线程池的依据:
- 任务的性质:
- CPU 密集型:尽可能少的线程,如 cpuNum + 1 个线程
- IO 密集型:尽可能多的线程,如 cpuNum * 2 个线程
- 混合型:如果任务可拆分,拆分成一个 CPU 密集型和一个 IO 密集型任务。如果两个任务执行时间差距小,则分解后执行的吞吐量将高于串行执行。如果两个任务执行时间差距大,则没必要分解
- 任务的优先级:高、中、低
- 优先级不同的任务可以使用优先级任务队列处理(可能存在优先级小的任务一直无法执行)
- 任务的执行时间:长、中、短
- 执行时间不同的任务可以交给不同规模的线程池处理,也可以使用优先级任务队列处理,让执行时间短的任务先执行
- 任务的依赖性:是否依赖其他系统资源,如数据库连接
- 依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,等待时间越长 CPU 空闲时间越长,那么可以设置更多的线程以充分利用 CPU 资源
建议使用有界队列,增加系统的稳定性和预警能力,避免因线程阻塞导致任务无限堆积,最终可能出现系统因内存不足而不可用(OOM)
2.5 线程池的监控
如果系统中大量使用线程池,则有必要对线程池进行监控。线程池监控相关方法:
getTaskCount()
方法:获取待执行的任务数getCompletedTaskCount()
方法:获取已执行完成的任务数getLargestPoolSize()
方法:获取曾经创建过的最大线程数getPoolSize()
方法:获取工作线程数getActiveCount()
方法:获取活跃(执行任务中)的线程数
还可以通过继承线程池来自定义线程池,重写 beforeExecute()
、afterExecute()
、terminated()
方法,也可以在任务执行前后、线程池关闭前执行一些代码来进行监控
以上是关于Java并发编程初识-线程池的主要内容,如果未能解决你的问题,请参考以下文章