什么是线程池
线程池是多个线程的集合,一旦有任务传给线程池,并且线程池中还有空闲线程的情况下,就会启动空闲线程执行该任务,执行结束之后,线程变为空闲状态等待下一个任务的执行。
为什么要使用线程池?
因为不断地创建线程销毁线程,会占用CPU的资源,减少CPU做其他有效工作的时间。线程池里的每一个线程任务结束后,并不会销毁,而是再次回到线程池中成为空闲状态,等待下一个对象来使用,因而借助线程池可以提高程序的执行效率。同时还可以控制线程的并发数量,避免大量并发导致内存溢出。
线程池的使用方式
在Java中创建一个线程池是基于ThreadPoolExecutor的构造方法,构造方法参数如下:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
可以看出,构造方法需要配置多个参数,它们各自代表的意义如下:
corePoolSize 线程池的核心线程数量。如果执行了线程池的
prestartAllCoreThreads()
方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize 线程池的最大线程数量,即最多能创建多少个线程
keepAliveTime 线程在没有任务执行之后多久销毁
unit 这个就是keepAliveTime的单位,比如秒、分、小时,详见TimeUnit
workQueue 用来存储等待执行任务的队列,当线程的数量超过corePoolSize时,会被加入到这个队列当中等待
threadFactory 用来创建线程
handler 当线程池中线程数量超出maximumPoolSize
时的处理策略
当执行一个新的任务时,它们之间的流程如下:
检查当前线程池的数量是否小于
corePoolSize
,如果小于corePoolSize
,就会创建一个新的线程处理该任务。如果大于等于corePoolSize
,但缓冲队列workQueue
还未满,就将该任务加入缓冲队列,如果缓冲队列有限定大小,且当前已达到队列的上限,就会检查当前线程数量,如果小于最大线程数maximumPoolSize
,就会创建新的线程,如果大于等于maximumPoolSize
,就会通过handler
所指定的拒绝策略来执行处理。
如下,通过ThreadPoolExecutor创建一个简单的线程池并执行任务:
val pool = ThreadPoolExecutor(5,10,60,TimeUnit.SECONDS,SynchronousQueue<Runnable>(),ThreadPoolExecutor.DiscardPolicy())
mPool.execute(Runnable {
//...执行任务
})
线程队列
线程池采用的队列是Java中的阻塞队列BlockingQueue,BlockingQueue
是一个接口,它的实现类有 ArrayBlockingQueue
、 DelayQueue
、 LinkedBlockingQueue
、 PriorityBlockingQueue
、 SynchronousQueue
等。一般线程池常用的队列类型主要有 ArrayBlockingQueue
、 LinkedBlockingDeque
、 SynchronousQueue
线程池常用的缓存队列有以下几种:
ArrayBlockingQueue
基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,所以需要在初始化时指定队列的大小。出队和入队共用同一个锁。
LinkedBlockingQueue
基于链表的阻塞队列实现,出队和进队分别采用独立的锁来控制数据同步,可以并行地操作入队和出队操作,提高整个队列的并发性能。如果初始化的时候没有指定大小,则默认为Integer.MAX_VALUE
的大小。
SynchronousQueue
没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列。采用这种队列时maximumPoolSizes
一般需要指定为Integer.MAX_VALUE
,否则可能会直接执行拒绝策略。
拒绝策略
前面说了,如果线程池的线程数量超过maximumPoolSizes
的大小,就会使用handler参数所配置的拒绝策略去处理,handler是一个RejectedExecutionHandler类型的对象,而RejectedExecutionHandler是一个接口,它有以下四个具体的实现类
ThreadPoolExecutor.AbortPolicy
采用此策略时线程池会丢弃当前任务,同时抛出一个RejectedExecutionException
的异常,打断当前的执行流程,同时这也是线程池默认的拒绝策略。
ThreadPoolExecutor.CallerRunsPolicy
采用此策略时会直接在 execute 方法调用时所处的线程中运行被拒绝的任务。
ThreadPoolExecutor.DiscardPolicy
采用此策略时会直接丢弃该任务。
ThreadPoolExecutor.DiscardOldestPolicy
采用此策略时,只要线程池没有关闭的话,丢弃队列中最先进去的一个任务,把最新的任务加入队列。
线程池的关闭
我们都知道线程有线程的中断方式,通过interrrupt去安全地处理一个线程的中断,线程池也有线程池的关闭方式,Java为我们提供了shutdownNow
和shuwdown
方法。
shutdownNow:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。
shutdown:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。
在讨论这两个方法之前,先了解一下线程池有哪些状态:
RUNNING 接受新任务,并且处理队列任务的状态
SHUTDOWN 不接受新任务,但是会处理队列任务的状态
STOP 不接受新任务,并且也不会处理队列任务的状态
TIDYING 所有线程池内线程都将被终止,并且将workCount清零,会运行终止线程池的方法
TERMINATED 运行终止线程池方法以及结束的状态
shutdownNow
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
查看 shutdownNow
的源码可以看到先将线程池的状态标记为STOP,然后再将当前线程池中的所有线程逐一调用 interrupt
方法。
shutdown
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
可以看到,跟 shutdownNow
的区别在于设置的状态是SHUTDOWN,然后同样遍历调用线程的 interrupt
方法,但是有一个关键点,这里判断了 tryLock
,tryLock
是尝试为线程加锁,返回true说明加锁成功,才会进一步调用 interrupt
方法,而线程池中只有正在运行的线程,会被lock住,所以正在运行的线程 tryLock
是会返回false的,也就不会被立即中断了。
所以综上两种关闭方法,我们需要根据不同的场景选择不同的处理方式,如果是选择 shutdown
关闭线程池,需要确认所有任务中没有会造成永久阻塞的场景,否则就会由于一直无法中断而导致无法关闭线程池。如果是采用 shutdownNow
的方式,则可能会由于突然中断抛出异常,需要进行对应的捕获处理。
四种常见的线程池
除了以上的自主配置的线程池,Java也为我们提供了几种常见的线程池,各自适用于一些常见的场景,如下:
CachedThreadPool:调用Executors.newCachedThreadPool()创建
FixedThreadPool:调用Executors.newFixedThreadPool()创建
ScheduledThreadPool:调用Executors.newScheduledThreadPool()创建
SingleThreadExecutor:调用Executors.newSingleThreadExecutor()创建
newCachedThreadPool
作用:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程,其构造参数如下:
public static Executors newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
原理:CachedThreadPool 的 corePoolSize 被设置为 0,maximumPoolSize 被设置为 Integer.MAX_VALUE,keepAliveTime 设置为 60,意味着 CachedThreadPool 中的空闲线程等待新任务的最长时间是 60 秒,空闲线程超过 60 秒后将会被终止。CachedThreadPool 使用没有容量的 SynchronousQueue 作为线程池的工作队列,但CachedThreadPool 的 maximumPool 是无界的。所以每次有新的任务进来,线程池由于corePoolSize为0,会不断将任务加入队列,但由于队列没有容量,但maximumPool无限大,所以进而不断创建新的线程去执行任务,且超过60秒空闲的话就销毁。
newFixedThreadPool
作用:创建一个可重用固定线程数的线程池,其构造参数如下:
public static Executors newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
原理:FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置成了相同的数量,keepAliveTime设置为0,以LinkedBlockingQueue作为缓存队列,但没有设置队列大小所以默认是无限大,所以每次有新的任务进来,如果小于corePoolSize会创建新的线程来执行任务,如果大于corePoolSize会不断将其加入到队列里面,直到有新的线程来执行队列中的任务。
newScheduledThreadPool
作用:创建一个可以延时或者定期执行任务的线程池,其构造参数如下:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
原理:传入一个参数设置核心线程数的大小,且使用DelayedWorkQueue作为任务队列,DelayedWorkQueue基于最小堆构造(父节点小于等于子节点,根节点元素是所有元素中最小的一个),可以看执行时间优先级排列,使得添加到队列中的任务,会按照任务的延时时间进行排序,延时时间少的任务首先被获取。
newSingleThreadExecutor
作用:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
原理:可以看到核心线程数和最大线程数都是1,所以这种线程池最多只会同时存在一个线程,且采用无限大小的LinkedBlockingQueue
队列,所以每次有新的任务进来,如果小于corePoolSize会创建一个线程来执行任务,如果大于corePoolSize会不断将其加入到队列里面,直到刚才那个线程执行完毕,才会从队列中取出下一个任务执行。
结语
合理地利用线程池处理多线程并发的场景,可以大大提高线程的复用率,也可以很方便地统一管理各个线程的生成和销毁,在Android中也有一些场景很适合用线程池去调度,比如一个有很多下载任务的下载列表,比如一组高频读写的数据库操作等等,灵活运用。
欢迎关注 Android小Y 的简书,更多Android精选原创
『Android自定义View实战』实现一个小清新的弹出式圆环菜单
『Android自定义View实战』玩转PathMeasure之自定义支付结果动画
『Android自定义View实战』自定义炫酷侧滑解锁效果
『Android自定义View实战』Android自定义带侧滑菜单的二维表格组件
GitHub:GitHubZJY
简 书:Android小Y
在GitHub上搭建了一个集合炫酷自定义View的项目 ZJYWidget ,里面有很多实用的自定义View源码及demo,会长期维护,欢迎Star~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~