一、线程池概念
1.1 为什么需要线程池
- 线程的创建和关闭需要消耗时间和资源
- 线程本身也需要占用内存空间,处理不当可能会导致OOM
- 大量线程的回收会给GC带来很大压力,延长GC的停顿时间
1.2 池化思想
1、可以提前创建一个池,里面总是有一些活跃线程;
2、当程序需要线程时,不创建线程,而是从线程池中获取线程;
3、当程序使用完线程后,不关闭线程,而是向线程池归还线程。
二、JDK对线程池的支持
2.1 Executor框架结构图
2.2 Executor框架提供的线程池
- public static ExecutorService newFixedThreadPool(int nThreads):返回一个固定线程数量的线程池
- public static ExecutorService newSingleThreadExecutor():返回一个只有一个线程的线程池
- public static ExecutorService newCachedThreadPool():返回一个根据实际情况调整线程数量的线程池
- public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize):返回一个支持定时或周期性执行任务的线程池,可以指定线程数量
- public static ScheduledExecutorService newSingleThreadScheduledExecutor():返回一个支持定时或周期性执行任务的线程池,线程数量为1
三、线程池的核心实现
3.1 构造函数
无论是newFixedThreadPool(),还是newSingleThreadExecutor()或newCachedThreadPool(),其内部实现均使用了ThreadPoolExecutor类
- 其最重要的构造函数如下所示:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
3.2 核心参数
- corePoolSize:指定线程池中核心线程数量
- maximumPoolSize:指定线程池中最大线程数量
- keepAliveTime:线程池的线程数量超过corePoolSize,多余线程的存活时间
- unit:keepAliveTime的单位
- workQueue:任务队列,存放被提交但未执行的任务
- threadFactory:线程工厂,用于创建线程
- handler:拒绝策略。当任务太多来不及处理时,如何拒绝任务
3.3 任务队列
workQueue是指被提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象
3.3.1 SynchronousQueue(直接提交的队列)
- 没有容量;
- 每一个插入操作都要等待对应的删除操作;每一个删除操作都要等待对应的插入操作;
- 处理任务的流程:提交的任务不会被保存,而是将新任务提交给线程执行,若没有空闲的线程,则尝试创建新线程,若线程数量已经达到最大,则执行拒绝策略。因此,使用synchronousQueue队列时一般将maximumPoolSize设置的比较大,否则很容易执行拒绝策略。
3.3.2 ArrayBlockingQueue(有界的任务队列)
- 使用此队列时,必须在创建时指定一个容量参数,表示队列的最大容量;
- 处理任务的流程(如下如所示):
- 有界队列,只有当任务队列满时,才可能将线程数提升到corePoolSize以上。
3.3.3 LinkedBlockingQueue(无界任务队列)
- 无需指定大小
- 处理任务的流程(如下如所示):
- 若任务创建的速度远远大于任务处理的速度,会导致无界队列快速增长,知道耗尽系统内存。
3.3.4 PriorityBlockingQueue(优先任务队列)
- 优先任务队列是带有执行优先级的队列。它通过PriorityBolckingQueue类实现,可以控制任务的执行先后顺序。
- 有界队列(ArrayBlockingQueue)和无界队列(LinkedBlokcingQueue)都是使用先进先出的算法处理任务;而优先任务队列(PriorityBlockingQueue)按照的是任务优先级来处理任务(总是保证高优先级的任务先执行)
3.4 默认线程池的的问题
1、newFixedThreadPool、newSingleThreadEcecutor()的问题
- 返回线程池的corePoolSize和maximumPoolSize大小相同;
- 同时使用无界队列LinkedBlockingQueue存放任务;
- 当任务提交速度大于处理速度时,该队列会一直增长,从而导致OOM。
2、newCachedThreadPool()的问题
- 返回线程池的corePoolSize大小为0,maximumPoolSize大小为无穷;
- 使用直接提交的队列SynchronousQueue;
- 当提交的速度大于任务的处理速度,线程池会不断地创建新的线程,从而导致OOM。
3.5 核心调度代码
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 当前线程数<corePoolSize,创建新线程执行
if (workerCountOf(c) < corePoolSize) { // workerCountOf()取得当前线程池的线程总数
if (addWorker(command, true))
return;
c = ctl.get();
}
// 进入等待队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 进入等待队列失败,提交给线程池
else if (!addWorker(command, false))
// 线程数量达到最大,执行拒绝策略
reject(command);
}
线程创建的时机:
四、拒绝策略
4.1 JDK内置拒绝策略
- AbortPolicy策略:直接抛出异常,阻止系统正常工作
- DiscardPolicy策略:直接丢弃无法处理的任务,同时不会产生任何提醒
- DiscardOledestPolicy策略:丢弃最老(即队列首节点)的请求,并再次尝试提交当前任务
- CallerRunsPolicy策略:只要线程池未关闭,就在调用者线程中运行当前被丢弃的任务。
4.2 自定义拒绝策略
- JDK内置的策略均实现了RejectExecutionHandler接口,我们可以通过实现此接口来自定义拒绝策略,其接口定义如下所示
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
4.3 代码示例
public class RejectThreadPoolDemo {
public static class MyTask implements Runnable {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + ":Thread.ID:" + Thread.currentThread().getId());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 1、创建任务
MyTask myTask = new MyTask();
// 2、创建线程池
ExecutorService pool = new ThreadPoolExecutor(
5, // 核心线程数
5, // 最大线程数
0L, // 存活时间
TimeUnit.MILLISECONDS, // 时间单位
new LinkedBlockingQueue<Runnable>(10), // 任务队列
Executors.defaultThreadFactory(), // 线程工厂
new RejectedExecutionHandler() { // 自定义拒绝策略
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(r.toString() + " is discard!");
}
}
);
// 三、提交任务
for (int i = 0; i < 100; i++) {
pool.submit(myTask);
Thread.sleep(10);
}
}
}
运行结果
五、ThreadFactory
自定义线程创建可以帮助我们
- 跟踪线程池再何时创建了多少线程
- 自定义线程名称
- 自定义线程优先级
- 将线程设置为守护线程
- 。。。
代码示例:
将线程池创建的所有线程都设置为守护线程,当主线程退出后,会强制销毁线程池
public class ExtendThreadFactory {
public static class MyTask implements Runnable {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + ":Thread.ID:" + Thread.currentThread().getId());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyTask myTask = new MyTask();
ExecutorService pool = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
// 自定义线程工厂,将线程都设置为守护线程
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
System.out.println("Create " + t);
return t;
}
});
for (int i = 0; i < 5; i++) {
pool.submit(myTask);
}
Thread.sleep(2000);
}
}
运行结果:
六、扩展线程池
虽然JDK已经帮我们实现了高性能的线程池,但我们有时想要对线程池做一些拓展功能例如:监控每个任务的开始和结束时间或者其他自定义的增强功能,此时我们就可以对线程池进行扩展
6.1 相关方法
ThreadPoolExecutor是一个可以扩展的线程池,它提供了三个接口对线程池进行控制,分别用于
- beforeExecute():记录一个任务的开始
- afterExecute():记录一个任务的结束
- terminated():记录整个线程池的退出
6.2 代码示例
public class ExtendThreadPool {
// 任务
public static class MyTask implements Runnable {
public String name;
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("正在执行:Thread.ID:" + Thread.currentThread().getId()
+ ",Task Name:" + this.name);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 主程序
public static void main(String[] args) throws InterruptedException {
// 一、创建线程池
ExecutorService pool = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()) {
// 任务开始前处理
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("准备执行:" + ((MyTask) r).name);
}
// 任务开始后处理
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("执行完成:" + ((MyTask) r).name);
}
// 整个线程池退出时的处理
@Override
protected void terminated() {
System.out.println("线程池退出!");
}
};
// 二、提交任务
for (int i = 0; i < 5; i++) {
MyTask task = new MyTask("TASK-" + i);
pool.execute(task);
Thread.sleep(10);
}
// 三、关闭线程池
pool.shutdown();
}
}
运行结果
七、优化线程池线程数量
- 线程池的线程数量太少,可能无法发挥最大的性能;而线程数量太多,上下文切换所消耗的时间和资源太多。所以,合理定义线程池的线程数量是非常重要的
- 经验公式
Ncpu=CPU的数量
Ucpu=目标CPU的使用率,0<=Ucpu<=1
W/C=等待时间与计算时间的比率
为保持处理器达到期望的使用率,最优的线程池线程数量大小为:
Nthreads=Ncpu*Ucpu*(1+W/C)