在实际的使用以及面试中时常碰到关于线程池的问题,但是线程池作为Java程序开发中的基础组件
拥有相当重要的地位,因此结合源码以及代码实验对线程池进行了探究,并用文字记录下来。
如有错误,不妥之处请各位大佬指出
- 线程池的核心参数
参数 | 类型 | 含义 |
---|---|---|
corePoolSize | int | 核心线程数 |
maximumPoolSize | int | 最大并行线程数 |
keepAliveTime | long | 非核心线程最大存活时间 |
unit | TimeUnit | 描述存活时间的单位 |
workQueue | BlockingQueue | 存放任务阻塞队列 |
threadFactory | ThreadFactory | 线程工厂 |
handler | RejectedExecutionHandler | 拒绝策略 |
corePoolSize: 线程池中的核心线程数,规定的是线程池中的常驻线程worker 的数量
maximumPoolSize:线程池的线程最大并行数量,规定的是线程池允许的最大的并行线程数量,在核心线程worker 已满 且 队列已满的情况下,会启动非核心的 线程worker 来执行任务
workQueue:核心执行线程已满时用于存放任务的阻塞队列,推荐使用带有边界的ArrayBlockingQueue
线程池最基本的运行单元是线程池中的一个内部内,Worker :
ThreadPoolExecutor extends AbstractExecutorService {
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// java.lang.Integer@Native public static final int SIZE = 32;
// 可见 Integer 是4个byte的长度, 使用了3位来标识线程池的状态,其余位数表示线程池
// 当前的worker的数量
private static final int COUNT_BITS = Integer.SIZE - 3;
// 线程池允许的最大容量,注意 是理论上可以容纳的最大的worker数量,
// 应该没有大佬会
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 线程池状态标识
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
//------------------------------------------------------------
// 获取线程池当前状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 获取线程池当前 worker 数量
private static int workerCountOf(int c) { return c & CAPACITY; }
// 通过位运算获得 线程池状态标识 + 线程池数量的 结果
private static int ctlOf(int rs, int wc) { return rs | wc; }
/*
因为继承了 AbstractQueuedSynchronizer,可知: worker 通过 cas的方式实现了线程安全
即 当我在接客的时候 不要往我的房间再塞人
*/
Worker extends AbstractQueuedSynchronizer implements Runnable {
// worker会从 ThreadFactory中获得一个线程对象
public void run() {
// 运行线程
runWorker(this);
}
// ... 其他方法是 创建线程对象,aqs方式对创建的线程进行运行,阻塞,中断等行为的方法
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
try {
// 线程池增强方法,加入线程池的前置操作,线程池监控常用
beforeExecute(wt, task);
Throwable thrown = null;
try {
/**
调用 runable中的run(),执行业务代码块
**/
task.run();
} catch (RuntimeException x) {
...
} finally {
// 线程池增强方法,完成任务之后的后置操作,线程池监控常用
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 记录执行结果,并将Worker移除在执行Set集合
processWorkerExit(w, completedAbruptly);
}
}
}
-
线程池调度方式
线程池的调度,我主要是指线程池当进入一个新任务时是如何完成整个生命周期的,
新的任务进入 =>
先查询核心worker数是否已满 ? 未满则新增核心worker执行任务 ,
已满则查看阻塞队列,阻塞队列未满则加入阻塞队列,
已满 且 查看最大并行线程数是否已满,
已满则调用拒绝策略,拒绝接受新增任务
未满则新增非核心worker处理该任务也就是说,
线程池的最大并行数是MAX_SIZE;
线程池的最大任务容量是MAX_SIZE + BlockingQueue.size()
同时,当我们设置阻塞队列的时候 如果采用 阻塞队列时如果 采用无边界的队列,或者不设置边界,在极端情况下对应用是危险的,会因为 Task的堆积发生OOM,这也是阿里大佬在小本本上要求自己去实现ThreadLocalPool 而不使用Executors中提供的线程池的原因。 -
线程池中的拒绝策略
实现自己的线程池拒绝策略就是这个接口,在线程池已满的情况下,我们可以把任务放进MQ,Redis,
应用内部队列中保存起来,找特定的时间窗口再去执行,当然,具体情况具体分析,反正接口,他就在那里
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
JDK大佬提供的拒绝策略:
AbortPolicy(默认):直接报错
DiscardPolicy:不报错,悄悄的就丢了,注意是悄悄的,什么反应都没有
DiscardOldestPolicy:不报错,悄悄的把队列中等得最久得丢了
CallerRunsPolicy:调用者自己处理
- 使用线程池的注意事项
1,注意初始化线程池时确定合理的并行数
2,不要使用没有边界的队列
3,拒绝策略需结合业务需求慎重选择,当然如果项目规模不大,且并发量低请忽略,默认即可,因为根本走不到那一步,但是咱不能不知道
4,码字不易,如果您老吃得好,欢迎再来,如果要端回家,麻烦给打个标,注明本店位置,下次再见