线程池的简单学习与使用
线程池的实现原理
当一个新的任务提交到线程池时
-
线程池判断核心线程池里的线程是否都在执行任务。
- 否。则创建一个新的工作线程来执行任务
- 是。则判断工作队列是否已满
- 否。则将任务存储在工作队列
- 是。线程池判断线程池的线程是否都处于工作状态
- 否。则创建一个新的工作线程来执行任务
- 是。则交给饱和策略来处理这个任务
-
流程图如下:
ThreadPoolExecutor
几乎所有的线程池框架都是通过ThreadPoolExecutor()来创建线程池的,创建线程池的代码如下:
new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,milliseconds,runnableTaskQueue,handler);
输入参数分析:
- corePoolSize:线程池的基本大小,即核心线程数,线程池空闲时也要保留在线程池的线程数
- runnableTaskQueue:任务队列,用于保存等待执行的任务的阻塞队列。常用的阻塞队列如下:
- ArrayBlockingQueue:一个基于数组结构的有界阻塞队列,按FIFO的规则对元素进行排序
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,也按照FIFO规则排序元素,吞吐量通常要高于ArrayBlockingQueue。后面会讲到的newFixedThreadPool在创建线程池时使用了该阻塞队列
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作都必须等到另一个线程调用移除操作,否则插入方法将一直被阻塞,吞吐量通常高于LinkedBlockingQueue。后面会讲到的newCachedThreadPool在创建线程池时使用了该阻塞队列
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列
- maximumPoolSize:线程池允许创建的最大线程数。
- ThreadFactory:用于设置创建线程的工厂
- RejectedExecutionHandler:饱和策略。当队列和线程池都满了,即线程池处于饱和状态时,所采取的处理提交的新任务的策略,默认为AbortPolicy,表示在无法处理新任务时直接抛出异常。线程池框架一共提供了以下策略:
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:只用调用者所在线程来运行任务
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
- DiscardPolicy:不处理,直接丢弃
- keepAliveTime:线程活动保持时间。即线程池的工作线程空闲后,保持存活的时间
- TimeUnit:线程活动保持时间的单位。可选项有DAYS(天),HOURS(小时),MINUTES(分钟),MILLISECONDS(毫秒),MICROSECONDS(微秒),NANOSECONDS(纳秒)
常见的线程池框架
FixedThreadPool
FixedThreadPool被称为可重用固定线程数的线程池。源码实现如下:
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
代码示例如下:
public class FixedThreadPoolDemo {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
fixedThreadPool.execute(() -> {
System.out.println("当前线程:" + Thread.currentThread().getName());
});
}
}
}
结果如下:
可以看出,线程池中不管什么时候最多都只有三个线程在运行
FixedThreadPoolde corePoolSize和maximumPoolSize都被设置为创建newFixedThreadPool时指定的参数nThreads。而keepAliveTime设置为0L,则意味着多余的空闲线程会被立即终止。
FixedThreadPool执行execute()方法的流程如下:
- 如果当前运行的线程数少于corePoolSize时,则创建新线程来执行任务
- 当前运行的线程数等于corePoolSize时,新提交的任务会加入LinkedBlockingQueue
- 线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue中获取任务来执行
FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列,队列容量为Integer.MAX_VALUE。使用无界队列作为工作队列会对线程池带来影响:
- 线程池中线程数大于corePoolSize时,新任务会在无界队列中等待,故线程池中的线程数不会超过corePoolSize
- 因为1,使用无界队列时maximumPoolSize是一个无效参数
- 因为1和2,使用无界队列时keepAliveTime是一个无效参数
- 由于使用无界队列,运行中的FixedThreadPool不会调用RejectedExecutionHandler.rejectedExecution()方法,即不会拒绝任务
SingleThreadExecutor
SingleThreadExecutor是使用单个worker线程的Executor,适用于需要顺序地执行各个任务,并且任意时刻都只有一个线程在线程池中运行。源码实现如下:
public static ExecutorService new SingleThreadExecutor(){
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())
);
}
SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1。其他参数与FixedThreadPool相同,同样使用无界队列LinkedBlockingQueue作为线程池的工作队列
代码示例如下:
public class SingleThreadExecutorDemo {
public static void main(String[] args) {
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
singleThreadPool.execute(() -> {
System.out.println("当前线程:" + Thread.currentThread().getName());
});
}
}
}
结果如下:
由上图可知,线程池里始终只有一个线程在执行任务。表面上看它和单线程没有什么区别。
但是当这个唯一的线程因为异常中断或者结束时,则会有一个新的线程来代替它。这点是单线程时做不到的。
CachedThreadPool
CachedThreadPool是一个会根据需要创建新的线程的线程池。
源代码实现如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
分析源码可以看出来,CachedThreadPool的corePooleSize被设置为0,最大线程数设置为Integer.MAX_VALUE,即无界。而工作队列则是使用的没有容量的SynchronousQueue。这就意味着,如果主线程提交任务的速度高于线程池中线程处理任务的速度时,线程池则会不断创建新线程。
在极端情况下,CachedThreadPool会因为创建过多线程而耗尽cpu和内存资源。
代码示例如下:
public class CachedThreadPoolDemo {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
cachedThreadPool.execute(() -> {
System.out.println("当前线程:" + Thread.currentThread().getName());
});
}
}
}
运行结果如下:
代码里我们提交了100个任务,但是运行结果最大线程数只有26。那是因为任务提交的速度比线程处理速度慢,线程在处理完任务后,用来处理新的任务(即线程复用),故不需要创建100个线程。
注意
在阿里巴巴java开发规范手册中,不推荐使用Executors工具类创建线程池,因为可能会产生内存溢出问题。