文章目录
概述
为什么需要使用池
- 我们平常使用线程的时候,都会通过new Thread()去创建一个新的线程,这样比较方便,但在实际开发时大多使用线程池。
- 线程的开启和销毁需要耗费系统资源,而频繁的创建销毁线程就会大大降低程序的效率。
- 程序的运行需要系统资源,而我们就需要去优化资源的使用,其中一种优化策略就是池化技术。
- 说到池大家一定多不陌生,线程池、JDBC连接池、对象池等都是经常使用的。池化技术通俗的讲就是实现准备好一些资源,有人要用的时候就来拿,用完就还,这样方便管理资源。
线程池的好处
- 降低资源的消耗。使用线程池可以避免重复开启线程,节省系统资源。
- 提高响应速度。由于事先开启了一定数量的线程,需要线程时可以直接从池中取来使用而不需要创建。
- 方便管理线程。创建线程池时我们可以指定最大线程数、等待队列等参数,避免因创建过多线程而造成内存溢出。
线程池的使用
如何创建线程池?
《阿里巴巴Java开发手册》中强制线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,避免资源好耗尽的风险。
Executors 返回线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为
Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。 - CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为
Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
1、通过ThreadPoolExecutor的构造方法实现
具体参数后面再讲。
2、通过Executors工具类创建线程池
Executors中大多方法都是用了static修饰,因此它是个工具类,通过Executors我们可以有三种方式创建线程池:
(1) Executors.newSingleThreadExecutor()
方法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先⼊先出的顺序执⾏队列中的任务。
程序测试:
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
// ExecutorService executor = Executors.newFixedThreadPool(10);
// ExecutorService executor = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown(); // 关闭线程池
}
}
}
执行结果:
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
...
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
该方式创建的线程池只有一个线程。
(2) Executors.newFixedThreadPool()
该⽅法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不变。当有⼀个新的任务提交时,线程池中若有空闲线程,则⽴即执⾏。若没有,则新的任务会被暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
程序测试:
public class ThreadPoolDemo {
public static void main(String[] args) {
// ExecutorService executor = Executors.newSingleThreadExecutor();
ExecutorService executor = Executors.newFixedThreadPool(10);
// ExecutorService executor = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown(); // 关闭线程池
}
}
}
执行结果:
...
pool-1-thread-5
pool-1-thread-3
pool-1-thread-5
pool-1-thread-10
pool-1-thread-8
pool-1-thread-7
...
该方式创建固定数量的线程池。
(3) Executors.newCachedThreadPool()
该⽅法返回⼀个可根据实际情况调整线程数量的线程池。
程序测试:
public class ThreadPoolDemo {
public static void main(String[] args) {
// ExecutorService executor = Executors.newSingleThreadExecutor();
// ExecutorService executor = Executors.newFixedThreadPool(10);
ExecutorService executor = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown(); // 关闭线程池
}
}
}
执行结果:
...
pool-1-thread-52
pool-1-thread-59
pool-1-thread-35
pool-1-thread-36
pool-1-thread-13
...
可以看出该方法创建的线程池中线程数量是随着任务量动态变化的,但当任务量过大的时候,创建过多的线程会导致系统效率低下,甚至内存溢出。
从上面三幅图中我们可以看到Executors工具类创建线程池时还是使用了ThreadPoolExecutor这个类去构造,相当于只是帮我们写好了参数,但在开发时我们应根据自己实际情况而配置参数,因此不建议使用方式2而是使用方式1手动指定参数。
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
该构造方法一共有七个参数:
- corePoolSize:核心线程数目
- maximumPoolSize:最大线程数目
- keepAliveTime:超时等待时间,时间到了自动释放
- unit:超时单位
- workQueue:工作队列
- threadFactory:线程工厂
- handler:拒绝策略
这里根据狂神的JUC视频(7大参数及自定义线程池)给出的例子来讲解7个参数。
假设现在有家银行,它总共开设了5个窗口,平常只有2个窗口是在运行的,另外3个是备用的,另外还设置一个等候区,该区域有3个座位。
当银行来人时首先就会安排开放的2个窗口来服务。
这时如果陆续来人了,但是备用窗口是没人客服服务的,因此它们只能在等候区等候。
这时候开放窗口和等候区都已经满了,因此银行只能要求备用窗口也开放用于处理更多业务。
但这时如果继续涌入人,银行怎么也无法再提供服务了,这时候就会拒绝服务,不再让更多的人涌入银行。
上面银行的业务处理和线程池的处理策略是一致的。
以下面创建的线程池对象为例:
public class ThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
5,
4,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
这里使用ThreadPoolExecutor创建了一个线程池对象。
- 2:核心线程数目,一开始只开启两个线程用于处理任务。
- 5:最大线程数目为5,因此除了两个核心线程外,还留有3个备用线程。
- 4:超时时间,当备用线程开启且处理完任务后,超过4秒没有新任务可执行,备用线程就会关闭。
- TimeUnit.SECONDS:超时单位为秒。
- new LinkedBlockingDeque<>(3):创建了一个链式阻塞队列,长度为3。
- Executors.defaultThreadFactory():创建线程的工厂,一般均采用此形式。
- new ThreadPoolExecutor.AbortPolicy()):忽略并弹出异常的拒绝策略。
该线程池的线程处理流程如下:
- 若任务数n为n<=2,则使用这两个核心线程处理。
- 若任务数n增长到2<n<=5,则用核心线程执行任务,并且另外几个任务停留在阻塞队列中。
- 若任务数n继续增长到5<n<8,则开启另外的3个备用线程来处理任务(备用线程=最大线程数-核心线程数)
- 若备用线程处理任务后,发现没有新的任务可供其执行(阻塞队列为空),则等待超时时间4秒,假设4秒内仍然没有新任务,则它会自动关闭。
- 若任务数n增长到n>8,则线程池此时所有线程以全部使用,且阻塞队列已满,启动拒绝策略,忽略新任务且抛出异常。
程序演示:
(1)任务数 < 核心线程数+阻塞队列大小
public class ThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
5,
4,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 0; i < 5; i++) { // 任务数为5,<= 2(核心线程数) + 3(阻塞队列大小)
threadPool.submit(()->{
System.out.println(Thread.currentThread().getName());
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
执行结果:
pool-1-thread-1
pool-1-thread-2
pool-1-thread-1
pool-1-thread-2
pool-1-thread-1
可以看到线程池只使用了2个核心线程执行任务。
(2)任务数 > (核心线程数+阻塞队列大小) 且 < (最大线程数 + 阻塞队列数)
public class ThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
5,
4,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 0; i < 7; i++) { // 任务数为7,大于5(核心线程数 + 阻塞队列大小) 且 小于8(最大线程数 + 阻塞队列大小)
threadPool.submit(()->{
System.out.println(Thread.currentThread().getName());
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
执行结果:
pool-1-thread-2
pool-1-thread-3
pool-1-thread-1
pool-1-thread-1
pool-1-thread-4
pool-1-thread-1
pool-1-thread-3
此时只开启了4个线程,因为任务数为7,有三个在阻塞队列中等待执行。
(3)任务数 > (最大线程数 + 阻塞队列数)
public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
5,
1,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 0; i < 9; i++) { // 任务数为9,大于(最大线程数 + 阻塞队列大小)
threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
执行结果:
pool-1-thread-1
pool-1-thread-5
pool-1-thread-4
pool-1-thread-3
pool-1-thread-2
pool-1-thread-5
pool-1-thread-4
pool-1-thread-1
java.util.concurrent.RejectedExecutionException:...
在线程池总任务数达到8时,线程数达到最大数目5,且阻塞队列已满,此时若继续添加任务则会拒绝服务并抛出异常:RejectedExecutionException
从源码角度简单分析线程池工作原理
public void execute(Runnable command) {
// 如果任务为null,则抛出NullPointerException异常
if (command == null)
throw new NullPointerException();
// 获取当前线程池的状态+线程个数变量的组合值
int c = ctl.get();
// (1)当前线程池线程个数是否小于核心线程数,小于则开启新线程运行
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// (2)如果线程池处于RUNNING状态,则添加任务到阻塞队列
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);
}
// (3)如果队列满了,则新增线程,新增失败则执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
这里跳过具体的细节,大概分析一下这段代码:
- 判断当前线程数是否小于核心线程数,如果小于则通过addWorker(command, true)新建⼀个线程,并将任务(command)添加到该线程中,然后启动该线程从而执⾏任务;否则调到步骤2
if (workerCountOf(c) < corePoolSize){
if (addWorker(command, true))
return;
...
}
- 如果线程池仍在运行,则将任务添加到工作队列中,不成功(队列已满)则调到步骤3
if (isRunning(c) && workQueue.offer(command)){
...
}
- 继续添加任务,此时会开启备用线程,若以达到最大限度,则会拒绝任务
else if (!addWorker(command, false))
reject(command);
这里从源码角度大概分析了一下线程池的工作原理,想要更加细致的源码分析可以参考这篇博客:
线程池之ThreadPoolExecutor线程池源码分析笔记,博主讲的十分细致。
拒绝策略
当线程池已经达到了所能承受的最大容量时,对于新来的任务必须采用一定的拒绝策略。
ThreadPoolTaskExecutor 定义⼀些策略:
(1)ThreadPoolExecutor.AbortPolicy()
如果线程池达到最大限度,不处理新增的任务且抛出RejectedExecutionException异常。具体情况如上面线程池执行过程中的拒绝过程。
(2)ThreadPoolExecutor.CallerRunsPolicy()
谁产生的任务就由谁去执行,由于任务是main线程产生的,因此线程池将该任务交由mian线程去执行。
public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
5,
1,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()); // CallerRunsPolicy拒绝策略
try {
for (int i = 0; i < 9; i++) { // 任务数为9,大于(最大线程数 + 阻塞队列大小)
threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
执行结果:
pool-1-thread-2
pool-1-thread-4
main
pool-1-thread-3
pool-1-thread-1
pool-1-thread-4
pool-1-thread-2
pool-1-thread-5
pool-1-thread-3
可以看到有个任务被main线程执行。
(3)ThreadPoolExecutor.DiscardPolicy()
对于新任务直接忽略,不抛出异常。
public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
5,
1,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy()); // DiscardPolicy拒绝策略
try {
for (int i = 0; i < 9; i++) { // 任务数为9,大于(最大线程数 + 阻塞队列大小)
threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
执行结果:
pool-1-thread-1
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4
pool-1-thread-5
(4)ThreadPoolExecutor.DiscardOldestPolicy()
尝试去和最早的进程竞争,不会抛出异常。
由于程序结果很难看出抢占的线程,这里不加演示。
如何设置线程池的大小
由于线程数量很影响系统的运行效率,因此设置一个合适的线程数量是十分有必要的。
这里以CPU密集型和IO密集型为分类设置线程池大小。
CPU密集型
电脑的核数是N核就选择N+1,设置maximunPoolSize参数。
线程并行(不是并发)执行时程序是很高的,因此将最大线程数设置为CPU核数可以取得较高的性能。+1使得当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费。
查看CPU核数:
可以通过任务管理器查看CPU核数,注意是看逻辑处理器数量。
在java程序查看CPU核数:
System.out.println(Runtime.getRuntime().availableProcessors());
结果:
8
创建线程池对象时我们可以这样创建:
int max = Runtime.getRuntime().availableProcessors(); // 获取CPU核数
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
max+1, // 最大线程数设置为CPU核数
1,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
IO密集型
由于IO型任务十分耗费系统资源,因此我们至少要为其分配线程,一般来说设置为2倍的CPU核数就可以了。
总结
线程池的使用归根到底可以用狂神老师的一句话概括:
三大方法、七大参数、四种拒绝策略。
- 三大方法
- newSingleThreadExecutor(); //单个线程
- newFixedThreadPool(5); //创建一个固定的线程池的大小
- newCachedThreadPool(); //可伸缩的线程池
- 七大参数
- corePoolSize:核心线程数目
- maximumPoolSize:最大线程数目
- keepAliveTime:超时等待时间,时间到了自动释放
- unit:超时单位
- workQueue:工作队列
- threadFactory:线程工厂
- handler:拒绝策略
- 四种拒绝策略
- ThreadPoolExecutor.AbortPolicy():忽略任务,抛出异常
- ThreadPoolExecutor.CallerRunsPolicy():哪来的去哪里执行
- ThreadPoolExecutor.DiscardPolicy():忽略任务,不抛出异常
- ThreadPoolExecutor.DiscardOldestPolicy():尝试去和最早的线程竞争,不抛出异常