文章目录
并发工具类-分类
- 为了并发安全∶互斥同步、非互斥同步、无同步方案
- 管理线程、提高效率
- 线程协作
线程池的自我介绍
为什么要使用线程池
-
问题一:反复创建线程开销大
-
问题二:过多的线程会占用太多内存
解决以上两个问题的思路
-
用少量的线程 ⇒ 避免内存占用过多
-
让这部分线程都保持工作,且可以反复执行任务 ⇒ 避免生命周期的损耗
线程池的重要性
-
治理线程的最大法宝 - 线程池
-
优点:复用线程,避免反复创建和销毁线程带来的开销问题,减少JVM回收垃圾的压力
线程池的好处
-
加快响应速度
-
合理利用CPU和内存
-
统一管理线程
线程池应用场景
-
服务器接受到大量请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率
-
实际上,在开发中,如果需要创建5个以上的线程,那么就可以使
用线程池来管理
创建和停止线程池
问题
- 线程池应该手动创建还是自动创建
- 线程池里的线程数量设定为多少比较合适?
线程池构造函数的参数
参数名 | 类型 | 含义 |
---|---|---|
corePoolSize | int | 核心线程数 |
maxPoolSize | int | 最大线程数 |
keepAliveTime | long | 保持存活时间 |
workQueue | BlockingQueue | 任务存储队列 |
threadFactory | ThreadFactory | 当线程池需要新的线程的时候,会使用threadFactory来生成新的线程 |
Handler | RejectedExecutionHandler | 由于线程池无法接受你所提交的任务的拒绝策略 |
-
corePoolSize:指的是核心线程数,线程池在完成初始化后,默认
情况下,线程池中并没有任何线程,线程池会等待有任务到来时,
再创建新线程去执行任务 -
maxPoolSize:线程池有可能会在核心线程数的基础上,额外增加一些线程,但是这些新增加的线程数有一个上限
-
keepAliveTime:如果线程池当前的线程数多于 corePoolSize,那么如果多余的线程空闲时间超过 keepAliveTime,它们就会被终止
-
workQueue:工作队列
有3种最常见的队列类型∶
-
直接交接:SynchronousQueue,需要将 maximumPoolSize 设置的大一些,因为这种队列本身不会进行任务缓存
-
无界队列:LinkedBlockingQueue,无需设置 maximumPoolSize,因为这种队列是一种无界队列,永远不会满,如果处理任务速度跟不上添加队列的速度,会导致队列中任务挤压严重
-
有界的队列:ArrayBlockingQueue,按需设置即可,maximumPoolSize 大于 corePoolSize
- ThreadFactory:用来创建线程
-
新的线程是由 ThreadFactory 创建的,默认使用 Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的 NORM_PRIORITY 优先级并且都不是守护线程。如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等。
-
通常我们用默认的ThreadFactory就可以了
线程池-线程数存储
- 添加线程规则
-
如果线程数小于 corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新线程来运行新任务。
-
如果线程数等于(或大于) corePoolSize 但少于 maximumPoolSize,则将任务放入队列。
-
如果队列已满,并且线程数小于 maxPoolSize,则创建一个新线程来运行任务。
-
如果队列已满,并且线程数大于或等于 maxPoolSize,则拒绝该任务。
线程池工作原理
- 是否需要增加线程的判断顺序是:
- corePoolSize
- workQueue
- maxPoolSize
举例说明
- 线程池∶核心池大小为5,最大池大小为10,队列为100。
- 因为线程中的请求最多会创建5个,然后任务将被添加到队列中,直到达到100
- 当队列已满时,将创建最新的线程 maxPoolSize,最多到10个线程,如果再来任务,就拒绝。
增减线程的特点
-
通过设置 corePoolSize 和 maximumPoolSize 相同,就可以创建固定大小的线程池。
-
线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它。
-
通过设置 maximumPoolSize 为很高的值,例如 Integer.MAX_VALUE,可以允许线程池容纳任意数量的并发任务。
-
只有在队列填满时才创建多于 corePoolSize 的线程,所以如果你使用的是无界队列(例如LinkedBlockingQueue),那么线程数就不会超过corePoolSize。
线程池的创建方式-线程池应该手动创建还是自动创建
手动创建更好,因为这样可以让我们更加明确线程池的运行规则,避免资源耗尽的风险。
让我们来看看自动创建线程池(也就是直接调用JDK封装好的构造函数)可能带来哪些问题
-
FixedThreadPool
由于传进去的 LinkedBlockingQueue 是没有容量上限的,所以当请求数越来越多,并且无法及时处理完毕的时候,也就是请求堆积的时候,会容易造成占用大量的内存,可能会导致OOM。
测试演示,修改启动配置:-Xmx8m -Xms8m
线程池-任务处理
- SingleThreadExecutor
-
单线程的线程池:它只会用唯一的工作线程来执行任务
-
它的原理和 FixedThreadPool 是一样的,但是此时的线程数量被设置为了1
也会导致OOM,可以看出,这里和刚才的 FixedThreadPool 的原理基本一样,只不过把线程数直接设成了1,所以这也会导致同样的问题,也就是当请求堆积的时候,可能会占用大量的内存。
- CachedThreadPool
-
可缓存线程池
-
特点:无界线程池,具有自动回收多余线程的功能
-
缺点:这里的弊端在于第二个参数 maximumPoolSize 被设置成 Integer.MAX_VALUE,这可能会创建数量非常多的线程,甚至导致OOM。
-
ScheduledThreadPool
支持定时及周期性任务执行的线程池
常见线程池的特点和用法
以上4种线程池的构造函数的参数
阻塞队列分析
-
FixedThreadPool 和SingleThreadExecutor 的 Queue 是LinkedBlockingQueue
-
CachedThreadPool 使用的 Queue 是 SynchronousQueue
-
ScheduledThreadPool来说,它使用的是延迟队列 DelayedWorkQueue
-
workStealingPool 是JDK1.8加入的
- 这个线程池和之前的都有很大不同
- 子任务:当前线程中嵌套其他任务
- 窃取:如果有线程处于空闲状态,会从其他线程的子任务中窃取线程来执行,但是不保证执行的顺序
正确的创建线程池的方法
- 根据不同的业务场景,自己设置线程池参数,比如我们的内存有多大,我们想给线程取什么名字等等
线程池里的线程数量设定为多少比较合适?
-
CPU密集型(加密、计算hash等):最佳线程数为CPU核心
数的1-2 倍左右。 -
耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于 cpu 核心数很多倍,以JVM线程监控显示繁忙情况为依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计算方法:线程数=CPU核心数(1+平均等待时间/平均工作时间)
停止线程池的正确方法
- shutdown
-
停止接收新的任务,如果往队列中添加任务时,会拒绝并抛出异常
-
将线程池中所有线程任务及队列中的任务全部执行完
不会立即停止线程池中所有任务
-
isShutdown
判断当前线程池是否停止,无法判断线程池中的任务是否执行完毕
-
isTerminated
判断当前线程池任务是否执行完毕
-
awaitTermination
等待一定时间后判断当前线程池中任务是否执行完成
- 如果线程池任务执行完毕会返回True
- 如果线程池中任务没有执行完,且已经超过了等待时间,返回False
- 如果被中断,则会抛异常
-
shutdownNow
立即停止正在执行的线程,并返回队列中未执行的任务
拒绝策略-任务太多,怎么拒绝?
拒绝时机
-
当 Executor 关闭时,提交新任务会被拒绝。
-
以及当 Executor 对最大线程和工作队列容量使用有限边界并且已经饱和时
4种拒绝策略
只有队列和线程池均满了,配置策略才会执行
-
AbortPolicy:直接抛出异常
-
DiscardPolicy:丢弃一些队列中的任务
-
DiscardOldestPolicy:丢弃最老未执行的任务
-
CallerRunsPolicy:反馈到提交任务线程,由提交任务线程去执行
钩子方法-给线程池加点料
- 每个任务执行前后
- 日志、统计
- 代码演示
package com.jensuper.prc.juc.threadpool;
import java.util.concurrent.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author jichao
* @version V1.0
* @description: 在线程池中,线程执行前后可以添加函数处理
* @date 2020/08/30
*/
public class PauseableThreadPoolTest extends ThreadPoolExecutor {
/**
* 锁
*/
private ReentrantLock lock = new ReentrantLock();
private Condition unPaused = lock.newCondition();
/**
* 暂停状态
*/
private boolean isPaused;
public PauseableThreadPoolTest(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public PauseableThreadPoolTest(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public PauseableThreadPoolTest(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public PauseableThreadPoolTest(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
// 执行之前需要判断暂停状态是否为 true
lock.lock();
try {
while (isPaused) {
unPaused.await();
}
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
/**
* 设置暂停方法
*/
private void pause() {
lock.lock();
try {
isPaused = true;
} finally {
lock.unlock();
}
}
/**
* 取消暂停方法
*/
private void resume() {
lock.lock();
try {
isPaused = false;
unPaused.signalAll();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
PauseableThreadPoolTest threadPoolTest =
new PauseableThreadPoolTest(10, 20, 6L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " => 执行方法");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 10000; i++) {
threadPoolTest.execute(runnable);
}
// 等待一定时间,设置线程状态为暂停
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadPoolTest.pause();
System.out.println(Thread.currentThread().getName()+" => 线程被暂停了");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadPoolTest.resume();
System.out.println(Thread.currentThread().getName()+" => 线程被唤醒了");
}
}
实现原理、源码分析
线程池组成部分
-
线程池管理器:创建线程、停止线程
-
工作线程:从队列中取出任务,用线程去执行任务
-
任务列队:存放未被线程执行的任务,需要支持并发
-
任务接口(Task ):每一个需要线程去执行的任务,这种任务最终要被放在队列中
Executor家族
线程池、ThreadPoolExecutor、ExecutorService、Executor、Executors等这么多和线程池相关的类,大家都是什么关系?
-
Executor:只有一个执行任务的方法
-
ExecutorService:继承了Executor,扩展了一些线程池的用法
-
Executors:工具类
线程池实现任务复用的原理
- 相同线程执行不同任务
- 源码分析
- 先判断工作线程小于核心线程数,则添加工作线程
- 每一个工作线程有 runWorker 方法,这个方法会循环从 task 队列中获取任务,并调用 task 的 run 方法,task 本身是 Runable
使用线程池的注意点
- 线程池状态
-
RUNNING:接受新任务并处理排队任务
-
SHUTDOWN:不接受新任务,但处理排队任务
-
STOP:不接受新任务,也不处理排队任务,并中断正在进行的任务
-
TIDYING:中文是整洁,理解了中文就容易理解这个状态了,所有任务都已终止,workerCount 为零时,线程会转换到 TIDYING 状态,并将运行 terminate() 钩子方法。
-
TERMINATED :terminate( ) 运行完成
- 注意点:
- 避免任务堆积
- 避免线程数过度增加
- 排查线程泄漏