文章目录
- 使用线程池的好处
- 线程池工作原理
- 自己实现一个简单的线程池
- ThreadPoolExecutor工作机制
- 构造方法
- 内部机制
- 拒绝策略
- AbortPolicy
- DiscardPolicy
- DiscardOldestPolicy
- CallerRunsPolicy
- 阻塞队列
- Executor框架 —— JDK提供的线程池家族
- 类图
- 几种开箱即用的线程池
- FixedThreadPool
- CachedThreadPool
- SingleThreadExecutor
- 合理配置线程池大小
- 任务的不同角度的分类
- 一个经验公式
- 实践配置
使用线程池的好处
首先,回答这个问题,要先回答为什么要使用多线程。
其次再是为什么要使用线程池。
使用多线程的好处及必要性就不说了。
使用线程池的好处:
- 提高响应速度:通过复用线程可以消除线程创建销毁带来的延迟,提示响应速度
- 降低资源消耗线程池可以统筹内存和CPU的使用,避免资源使用不当,线程池会根据配置和任务数量灵活控制线程数量,不够就创建,多了就回收,避免线程过多导致内存溢出,过少导致资源浪费
- 提高线程可管理行线程池可以统一管理资源,统一进行分配、调优、监控。
线程池工作原理
将任务传递给线程池,而不是为每个任务启动一个新线程来并发执行。
一旦池中有任何空闲线程,任务就会分配给其中之一并执行。
在内部,任务被插入到一个阻塞队列中,然后池中的空闲线程从该队列中取出任务并执行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1vybJWnB-1634477489379)(thread-pools.png)]
不过在JDK提供的线程池的原理比这个还要复杂些(真实的往往是复杂的)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-paqxFCjB-1634477489395)(pool2.jpg)]
自己实现一个简单的线程池
/**
* @author: SongyangJi
* @description:
* @since: 2021/10/15
*/
public class ThreadPool {
private final BlockingQueue<Runnable> taskQueue;
private final List<PooledThread> threads = new ArrayList<>();
private volatile boolean isStopped = false;
/**
* @param numOfThreads 工作线程数
* @param maxNumOfTasks 缓冲队列最多预存任务数
*/
public ThreadPool(int numOfThreads, int maxNumOfTasks) {
taskQueue = new LinkedBlockingQueue<>(maxNumOfTasks);
for (int i = 0; i < numOfThreads; i++) {
threads.add(new PooledThread(taskQueue));
}
for (PooledThread thread : threads) {
thread.start();
}
}
public void execute(Runnable task) throws InterruptedException {
if (this.isStopped) throw
new IllegalStateException("ThreadPool is stopped");
this.taskQueue.put(task);
}
public void shutDown() {
this.isStopped = true;
for (PooledThread thread : threads) {
thread.toStop();
}
taskQueue.clear();
}
private static class PooledThread extends Thread {
private final BlockingQueue<Runnable> taskQueue;
private volatile boolean isStopped = false;
public PooledThread(BlockingQueue<Runnable> queue) {
taskQueue = queue;
}
@Override
public void run() {
while (!isStopped) {
Runnable runnable;
try {
runnable = taskQueue.take();
runnable.run();
} catch (InterruptedException ignored) {
}
}
}
public void toStop() {
isStopped = true;
this.interrupt(); // 中断当前线程(但可能不被响应)
}
}
// 测试
public static void main(String[] args) throws InterruptedException {
Random random = new Random();
ThreadPool threadPool = new ThreadPool(3, 10);
for (int i = 0; i < 20; i++) {
final int id = i;
threadPool.execute(() -> {
try {
TimeUnit.MILLISECONDS.sleep(random.nextInt(3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("task %d finished in %s\n", id,Thread.currentThread().getName());
});
}
TimeUnit.SECONDS.sleep(10);
threadPool.shutDown();
}
}
ThreadPoolExecutor工作机制
构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
其中构造参数的含义为:
序号 | 名称 | 类型 | 含义 |
1 | corePoolSize | int | 核心线程池大小 |
2 | maximumPoolSize | int | 最大线程池大小 |
3 | keepAliveTime | long | 线程最大空闲时间 |
4 | unit | TimeUnit | 时间单位 |
5 | workQueue | BlockingQueue | 线程等待队列 |
6 | threadFactory | ThreadFactory | 线程创建工厂 |
7 | handler | RejectedExecutionHandler | 拒绝策略 |
内部机制
- 如果当前线程池的线程数还没有达到基本大小(poolSize < corePoolSize),无论是否有空闲的线程新增一个线程处理新提交的任务;
- 如果当前线程池的线程数大于或等于基本大小(poolSize >= corePoolSize) 且任务队列未满时,就将新提交的任务提交到阻塞队列排队,等候处理workQueue.offer(command);
- 如果当前线程池的线程数大于或等于基本大小(poolSize >= corePoolSize) 且任务队列满时;
- 当前poolSize<maximumPoolSize,那么就新增线程来处理任务;
- 当前poolSize=maximumPoolSize,那么意味着线程池的处理能力已经达到了极限,此时需要拒绝新增加的任务。至于如何拒绝处理新增的任务,取决于线程池的饱和策略RejectedExecutionHandler。
描述为如下的工作流程图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3agJFUtj-1634477489397)(pool1.jpg)]
拒绝策略
拒绝策略是RejectedExecutionHandler的实现类;
// r - 请求执行的可运行任务
// executor – 尝试执行此任务的执行器
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
当要创建的线程数量大于线程池的最大线程数的时候,新的任务就会被拒绝,就会调用这个接口里的这个方法。
下面介绍 ThreadPoolExecutor 中已经实现的四种策略。
AbortPolicy
这也是默认的拒绝策略(保守的策略)
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
DiscardPolicy
很潇洒,啥也不做。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
DiscardOldestPolicy
丢弃最早的未处理请求,然后去执行这个现在的任务。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
CallerRunsPolicy
在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务。
也就是说,自己的活自己做,不要让线程池去做了。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
阻塞队列
阻塞队列的选取可以参考在JUC中BlockingQueue的几种实现类,这里不再赘述。
Executor框架 —— JDK提供的线程池家族
类图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZzYJLxTX-1634477489398)(ThreadPoolExecutor.png)]
几种开箱即用的线程池
FixedThreadPool
构造函数:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特点:
- workQueue 为LinkedBlockingQueue(无界阻塞队列),队列最大值为Integer.MAX_VALUE。如果任务提交速度持续大余任务处理速度,会造成队列大量阻塞。因为队列基本上是无穷大,很有可能在拒绝策略前,内存溢出。由于队列容量无穷大,所以实际上maximumPoolSize是无效参数。
- corePoolSize与maximumPoolSize相等,即其线程全为核心线程,是一个固定大小的线程池;
- keepAliveTime = 0 表明多余的线程会被立即终止,但是由于默认情况下这不对核心线程起作用,又由于队列容量无穷大,所以这个参数实际上也是无效的。(因为在核心线程池满之后,就不再被销毁,除非你
allowCoreThreadTimeOut(true)
) - 由于第一点,没有设置拒绝策略,设置了也是无效的。
适用场景:
计算重量级的任务。
CachedThreadPool
构造函数:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制;
- keepAliveTime = 60s,线程空闲60s后自动结束,因此长时间保持空闲的CachedThreadPool不占用资源。
- workQueue 为 SynchronousQueue 为无容量的同步队列,这个队列类似于一个接力棒,入队出队必须同时传递,所以当核心线程池已满的时候,会直接创建一个新的线程;
适用场景:快速处理大量耗时较短的任务,如Netty的NIO接受请求时,可使用CachedThreadPool。
SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
实际上就是 Executors.newFixedThreadPool(1)的包装。
线程池一旦接受了一个任务,就保持一个线程去接受任务。
合理配置线程池大小
任务的不同角度的分类
合理的配置线程池的大小,从以下几个角度分析任务的特性:
- 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。
- 任务的优先级:高、中、低。
- 任务的执行时间:长、中、短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接等。
性质不同的任务可以交给不同规模的线程池执行
- CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数;
- IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1;
- 混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。
一个经验公式
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 ) CPU数目*
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:
最佳线程数目 = (线程等待时间/线程CPU时间+ 1)* CPU
可以得出一个结论: 线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。 以上公式与CPU和IO密集型任务设置线程数基本吻合。
实践配置
高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
- 高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
- 并发不高、任务执行时间长的业务要区分开看:
- 假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务
- 假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
- 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。
最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦