线程池
文章目录
- 线程池
- 1.线程池存在意义
- 2.线程池分类
- 分割线——以下内容为原理部分,主要涉及面试,链接[Java面试经典题:线程池专题](https://juejin.cn/post/6844903634975768590),最好结合源码去理解
- 3. 线程池执行流程,来自[github](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/multi-thread/java%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md)
- 4.**直接去看源码**——[线程池如何实现复用](https://cloud.tencent.com/developer/article/1652672) + [获取task到运行的细节实现](https://www.jianshu.com/p/8b3979a492ca)
1.线程池存在意义
创建线程需要操作系统资源(线程资源,栈空间等),存在以下问题:
- 频繁创建和销毁大量线程(GC)需要消耗大量时间。
- 多线程抢占资源,缺乏管理造成页面卡顿
线程池:能接受大量小任务并进行分发处理的就是线程池
简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
2.线程池分类
线程池接口ExecutorService,具体实现类——FixedThreadPool、CachedThreadPool、SingleThreadExecutor
典型用法如下:
// 创建固定大小的线程池
ExecutorService executor = Executores.newFixedThreadPool(3);
// 提交任务
executor.submit(new task1());
executor.submit(new task2());
executor.submit(new task3());
executor.submit(new task4());
executor.submit(new task5());
// 关闭线程池
executor.shutdown();
class Task1 implements Runable {
}
shutdown()
会先等待正在执行的任务完成后,再关闭线程池
shutdownNow()
立刻停止正在执行的任务
2.1 FixedThreadPool:线程数固定的线程池
举例:线程池大小为6,executor执行4个进程后,新来的任务需等待其他进程执行结束
2.2 CachedThreadPool:线程数根据任务动态调整的线程池
- 线程数无限制
- 有空闲线程则复用空闲线程,若无空闲线程则新建线程 一定程序减少频繁创建/销毁线程,减少系统开销
经典调用:
ExecutorService executor = Executores.newFixedThreadPool();
需注意动态线程池的范围在0~Integer.MAX_VALUE之间,见源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
因此如果想创建指定动态范围的线程池可以这么写:
int min = 4;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(min, max,
60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
2.3 SingleThreadExecutor:仅单线程执行的线程池
- 所有任务按照指定顺序执行,即遵循队列的入队出队规则
2.4 ScheduledThreadPool:定期反复执行任务的线程池
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
// 1秒后执行一次性任务:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
// 2秒后开始执行定时任务,每3秒执行:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
// 2秒后开始执行定时任务,以3秒为间隔执行:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
- FixedRate是指任务总是以固定时间间隔触发
- FixedDelay是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务
分割线——以下内容为原理部分,主要涉及面试,链接Java面试经典题:线程池专题,最好结合源码去理解
链接中补充内容:
两个处理queue:
- 处理queue——其实就是各个执行的work线程,线程的大小写在ctl的CAPACITY里面。work也会存在
HashSet<Worker> workers
- 阻塞queue——BlockingQueue,实例化为workQueue
区分清楚三个东西
- corePoolSize核心线程池大小
- maximumPoolSize线程池最大支持大小
- 处理队列——workQueue
处理逻辑是这样的,每次来一个任务先保证线程数到达corePoolSize,再来的任务都放置处理队列等待workQueue。
当队列满了存不下的时候,并且线程数小于maximumPoolSize,那么新建线程(非核心)执行任务
如果线程数到了maximumPoolSize,就说明,现在来的任务我没地方存了,也没线程执行了,只能抛出异常了
线程池状态和容量管理——AtomicInteger ctl
AtomicInteger表示原子操作的int类型,integer一共32位,高3位存储状态,低29位存储容量
AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
int COUNT_BITS = Integer.SIZE - 3; //COUNT_BITS=32
int CAPACITY = (1 << COUNT_BITS) - 1; //高3位000,低29位全为1
int RUNNING = -1 << COUNT_BITS; //高3位111,低29位全为0
int SHUTDOWN = 0 << COUNT_BITS; //高3位000,低29位全为0
int STOP = 1 << COUNT_BITS; //高3位001,低29位全为0
int STOPPING = 2 << COUNT_BITS; //高3位010,低29位全为0
int TERMINATED = 3 << COUNT_BITS; //高3位011,低29位全为0
//得到运行状态
int runStateOf(int c) {
return c & ~CAPACITY; //c 与 CAPACITY的取反
}
//得到工作线程数量
int workerCountOf(int c) {
return c & CAPACITY;
}
//初始化ctl
private static int ctlOf(int rs, int wc) {
return rs | wc;
}
3. 线程池执行流程,来自github
ThreadPoolExecutor
3 个最重要的参数:
corePoolSize
: 核心线程数线程数定义了最小可以同时运行的线程数量。maximumPoolSize
: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor
其他常见参数:
keepAliveTime
:当线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁;unit
:keepAliveTime
参数的时间单位。threadFactory
:executor 创建新线程的时候会用到。handler
:饱和策略。关于饱和策略下面单独介绍一下。
ThreadPoolExecutor
饱和策略定义:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor
定义一些策略:
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理。ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
4.直接去看源码——线程池如何实现复用 + 获取task到运行的细节实现
线程执行优先级:先核心线程——阻塞队列workQueue
——阻塞队列满才会去考虑最大线程数 maximumPoolSize
当前运行线程小于核心线程数,则优先创建新的线程,创建和销毁的时候都要获取一个ReentrantLock 重入锁。
从源码的角度理解,不贴源码了,自己去看ThreadPoolExecutor
里面execute
,关键就是addWorkers
核心线程通过addWorkers
创建,核心线程类Worker
封装了Thread。创建worker时传入了第一个runnable的task,然后就执行了worker.thread.start()
,去执行worker对应线程里面的run方法
public void run() {
runWorker(this);
}
补充知识点,
private final class Worker extends AbstractQueuedSynchronizer
implements Runnable
worker里面重写了runnable里面的run,整个复用过程设涉及到的就是Worker实例化和Worker里面的run
实际上执行的则是runWorker
。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
//step1:第一次进入的时候task为firstTask,然后执行,往后都是从阻塞队列workQueue中取task
//getTask的细节见下面
//通过while循环,不停的取task,不停的执行runnable
while (task != null || (task = getTask()) != null) {
//加锁和解锁的环节
w.lock();
//step2:预检查,不满足条件则直接中断,详情还是见源码
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
//step3:关键处,执行task的run
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.略
// Are workers subject to culling?略
try {
//每次从workQueue里面取task,workQueue的class是BlockingQueue——线程安全,
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}