为何需要线程池
服务器端,经常面对的是客户端传入的短小任务,需要服务端快速处理并返回结果。然而,如果面对成千上万的任务,如果服务端每次一个任务都创建一个线程,这样会导致系统频繁地进行线程上下文切换,创建和消亡线程也需要大量时间。所以,这时候就有了线程池。
大概
线程池是预先创建了若干数量的线程,并且不能由用户直接对线程的创建进行控制,在此前提下重复使用固定或者较为固定数目的线程来完成任务。这样做的好处有三:
- 降低资源消耗——重复利用已创建线程,减少由于线程创建或销毁带来的时间消耗
- 提高响应速度——线程已创建直接工作
- 提供线程可管理性
线程池的本质是使用了一个线程安全的工作队列连接工作者线程和客户端线程,客户端线程将任务放入工作队列后便返回,而工作者线程则不断从工作队列上取出工作并执行;工作队列为空时,所有工作者线程均等待在工作队列上,有客户端提交任务会通知工作线程(或者工作线程自动获取到)。
ThreadPoolExecutor设计思路:
- 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(获取全局锁)
- 如果运行的线程等于或多于corePollSize,则将任务加入BlockingQueue,当某一线程处理完任务后会从队列中获取任务
- 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(获取全局锁)
- 如果创建新线程将导致当前运行的线程数超过maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法(饱和策略)。
线程池几个参数说明(其构造函数的参数)
-
corePoolSize(线程池的基本大小)
当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的线程能够执行新任务也会创建线程,直到需要执行的任务数大于线程池基本大小。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
public int prestartAllCoreThreads() { int n = 0; while (addWorker(null, true)) ++n; return n; }
addWorker后面再分析
-
runnableTaskQueue(任务队列):
用于保存等待执行的任务的阻塞队列。可以选择-
ArratBlockingQueue
有界队列,FIFO
-
LinkedBlockingQueue
基于链表,吞吐量高(较上者),无界队列,FIFO
-
SynchronousQueue
不存储元素的阻塞队列,每个插入都需要等待线程调用移除操作(已有空闲线程或者新建线程),否则处于阻塞状态,吞吐量高(较上者)
-
PriorityBlockingQueue。
优先级,无界队列
-
-
maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。
-
ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
-
RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。策略有下列几种:
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
- 其他应用场景需要实现RejectedExecutionHandler接口自定义。
-
keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用。如果任务多且执行时间短,可以调高存活时间提高线程利用率。
如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0
-
TimeUnit(线程活动保持时间的单位)
向线程池提交任务
-
execute()
提交不需要返回值的任务
-
submit()
提交需要返回值的任务,线程池会返回一个future类型的对象,通过此对象判断是否执行成功, future.get()获取返回值(若当前线程未执行完会阻塞),get(long timeout, TimeUnit unit)设置等待时间
关闭线程池
以下两个方法的原理都是遍历线程池中的工作线程,组个调用interrupt()终端线程,因此无法响应中断的任务可能永远无法终止
-
shutdown()
将线程池的状态设置成SHUTDOWN状态,然后中断没有正在执行任务的线程。
-
shutdownNow()
将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
只要调用了两种关闭方法的任一种,isShutdown()方法都会返回true。当且仅当所有任务都关闭,这时isTerminaed()方法才会返回true。
合理配置线程池(设N为CPU个数)
- CPU密集型任务,应配置尽可能少的线程,如N+1。
- IO密集型任务,应配置尽可能多的线程,如2N。
- 优先级不同的任务可以考虑使用优先级队列priorityBlockingQueue来处理,但优先级低的任务可能永远不被执行。
- 使用有界队列能增加系统的稳定性和预警性,避免队列越来越多撑满内存,导致系统不可用。
线程池的监控
可以使用以下属性:
- taskCount:线程池需要执行的任务数量。
- completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
- largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。
- getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。
- getActiveCount:获取活动的线程数。
可以通过继承线程池来自定义线程池,重写线程池的beforeExecute, afterExecute和terminated方法,也可以在任务执行前后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。这几个方法在线程池里都是空方法。
参考:
《java并发编程的艺术》
网友评论