本文是学习Java多线程与高并发知识时做的笔记。
这部分内容比较多,按照内容分为5个部分:
- 多线程基础篇
- JUC篇
- 同步容器和并发容器篇
- 线程池篇
- MQ篇
本篇为线程池篇。
目录
1 线程池简介
线程池(thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能,而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。使用线程池避免了在处理短时间任务时创建与销毁线程的代价。线程池中的线程数也不宜过多,否则会导致额外的线程切换开销,线程数一般取CPU数量+2比较合适。
使用线程池的优势:
- 降低资源的消耗:通过重复利用已经创建好的线程降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,不需要等待线程创建就能立刻执行。
- 方便管理线程:对线程进行统一的分配、调优和监控。
即 线程复用、控制最大并发数、管理线程。
关于线程池必须知道的是:三种线程池、七大参数、四种拒绝策略。
2 三种线程池
java.util.concurrent包中提供了一个工具类Executors。
所谓的三种线程池是指java.util.concurrent.Executors类中定义的三种线程池:
- 单线程化线程池
- 定长线程池
- 可缓存线程池
2.1 单线程化线程池
单线程化线程池中线程数为1,所有的工作任务都会由这个唯一的线程来执行。
创建方法:
ExecutorService threadPool = Executors.newSingleThreadExecutor();
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> { //使用线程池来执行多线程任务
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} finally {
threadPool.shutdown(); //线程池一定要关闭
}
}
}
运行结果:
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
2.2 定长线程池
定长线程池 可以控制线程数目,以此来控制线程的最大并发数。
创建方法:
ExecutorService threadPool = Executors.newFixedThreadPool(最大线程数);
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> { //使用线程池来执行多线程任务
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} finally {
threadPool.shutdown(); //线程池一定要关闭
}
}
}
运行结果:
pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-3 ok
pool-1-thread-3 ok
pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-2 ok
pool-1-thread-1 ok
2.3 可缓存线程池
可缓存线程池,由线程池自适应调整任务需要的线程数。
自适应:当线程池要开启新的线程任务时,若线程池中有空闲线程,则由空闲线程来处理任务,若没有空闲线程,则新建线程。
创建方法:
ExecutorService threadPool = Executors.newCachedThreadPool();
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> { //使用线程池来执行多线程任务
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} finally {
threadPool.shutdown(); //线程池一定要关闭
}
}
}
运行结果:
pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-6 ok
pool-1-thread-5 ok
pool-1-thread-1 ok
pool-1-thread-5 ok
2.4 注意事项
需要注意的是:在《阿里巴巴Java开发手册中》,【强制】要求线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式(自定义线程池),这样的处理方法让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors返回的线程池对象的弊端如下:
- FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
3 七大参数
3.1 线程池参数
我们试着分析三种线程池创建方法的源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
发现它们都返回了一个ThreadPoolExecutor对象。
查看ThreadPoolExecutor类的构造器:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
可以看到定义线程池的七个参数:
- int corePoolSize,核心线程池大小。
- int maximumPoolSize,最大线程池大小。
- long keepAliveTime,保持活跃时间。
- TimeUnit unit,保持活跃时间的单位。
- BlockingQueue<Runnable> workQueue,工作(阻塞)队列。
- ThreadFactory threadFactory,线程工厂。
- RejectedExecutionHandler handler,拒绝执行处理程序。
接下来讲一个 银行营业厅 的模型,方便大家理解线程池中的各个参数的意义。
从前有一个银行营业厅,【线程池】
这个银行营业厅有5个窗口,【最大线程池大小=5】
平时业务清闲时,只有2个窗口办理业务。【核心线程池大小=2】
营业厅内设有候客区,候客区有3个座位。【工作(阻塞)队列,大小=3】
业务繁忙的时候,有一次,
先来了2位顾客,他们一来就到2个常开的窗口办理业务,
又来了3位顾客,他们发现所有开放的窗口都有人正在办理业务,就坐在候客区的座位上等待,
又来了1位顾客,营业厅发现开放的窗口、候客区都没有位置了,就又打开1个窗口来营业,
又来了2位顾客,营业厅的所有窗口全部开放营业,候客区坐满,
又来了1位顾客,营业厅拒绝为他提供服务。【拒绝执行处理程序】
业务繁忙的时段过去以后,顾客进来的速度不再能赶上所有开放窗口的处理速度,
有一个窗口有1个小时都没有顾客去办理业务,【保持活跃时间=1,保持活跃时间单位=小时】
这个窗口就关闭了。
在上面的模型中唯独没有提到线程工厂,它是用来创建线程的。【工厂模式】
这里使用线程工厂是为了统一在创建线程时设置一些参数,一般不需要修改。
关于 拒绝执行处理程序,我们到 四种拒绝策略 章节再进一步讲解。
3.2 自定义线程池
在了解了线程池的七大参数后,我们可以通过new ThreadPoolExecutor(,,,,,,)的方式创建自定义线程池。在《阿里巴巴Java开发手册中》,【强制】要求使用这种方式创建线程池。
创建一个自定义线程池:
import java.util.concurrent.*;
public class BusinessHall {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(2, //核心线程池大小=2
5, //最大线程池大小=5
2, //保持活跃时间=2
TimeUnit.SECONDS, //保持活跃时间单位=秒
new LinkedBlockingDeque<>(3), //工作(阻塞)队列,大小=3
Executors.defaultThreadFactory(), //默认线程工厂
new ThreadPoolExecutor.AbortPolicy() //一种拒绝执行处理程序
);
try {
for (int i = 0; i < 8; i++) { //保证没有任务被拒绝执行的最大线程数=5+3
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} finally {
threadPool.shutdown(); //线程池一定要关闭
}
}
}
运行结果:
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-4 ok
pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-5 ok
pool-1-thread-2 ok
工作中创建自定义线程池,最大线程池大小设多少比较合适?
首先评估活跃进程属于CPU密集型还是IO密集型:
- CPU密集型:程序的大多数时间花在计算上。
- IO密集型:程序的大多数时间花在input和output上。
如果是CPU密集型程序,就将最大线程池大小设为服务器逻辑处理器的数目。
获取服务器逻辑处理器数目的API:
public class Test { public static void main(String[] args) { System.out.println(Runtime.getRuntime().availableProcessors()); } }
如果是IO密集型,评估程序中有多少条比较耗费IO资源的线程,只要比这个数目大就ok,可以设为2倍。
4 四种拒绝策略
线程池拒绝执行处理程序共有四种拒绝策略:
- AbortPolicy,默认。
- CallerRunsPolicy
- DiscardPolicy
- DiscardOldestPolicy
4.1 AbortPolicy
当任务数目超过 最大线程池大小与工作(阻塞)队列大小之和 时,抛出异常:
java.util.concurrent.RejectedExecutionException
代码测试:(可能不会报错,多测几次)
import java.util.concurrent.*;
public class BusinessHall {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(2,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() //拒绝执行处理程序
);
try {
for (int i = 0; i < 10; i++) { //任务数目超过最大线程池大小于工作(阻塞)队列大小之和
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} finally {
threadPool.shutdown(); //线程池一定要关闭
}
}
}
运行抛出异常:
Exception in thread "main" java.util.concurrent.RejectedExecutionException
4.2 CallerRunsPolicy
当任务数目超过 最大线程池大小与工作(阻塞)队列大小之和 时,将超出的任务返回到任务来源的线程执行。
代码测试:
import java.util.concurrent.*;
public class BusinessHall {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(2,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy() //拒绝执行处理程序
);
try {
for (int i = 0; i < 10; i++) { //任务数目超过最大线程池大小于工作(阻塞)队列大小之和
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} finally {
threadPool.shutdown(); //线程池一定要关闭
}
}
}
运行结果:
pool-1-thread-1 ok
pool-1-thread-3 ok
main ok
pool-1-thread-4 ok
pool-1-thread-2 ok
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-5 ok
pool-1-thread-3 ok
pool-1-thread-1 ok
4.3 DiscardPolicy
当任务数目超过 最大线程池大小与工作(阻塞)队列大小之和 时,将超出的任务舍弃。不会抛出异常。
4.4 DiscardOldestPolicy
当任务数目超过 最大线程池大小与工作(阻塞)队列大小之和 时,将工作(阻塞)队列中最老的任务舍弃,超出的任务进入队列。不会抛出异常。
学习视频链接:
https://www.bilibili.com/video/BV1B7411L7tE
加油!(ง •_•)ง