文章目录
前言
随着我们工作经验的提升,以及业务复杂度的提升。有时候单线程的使用并不能满足我们方法执行效率的要求,这时候可能就需要引入多线程来提升效率,为了更好的使用和管理多线程,线程池往往是我们工作中最常用的手段
1、线程池是什么?
线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。
线程如果过多的话,频繁的调度、创建、销毁会造成过多的额外的花销。所以线程池的存在可以帮助我们管理和调度多个线程,可以使多个任务并发执行,提高我们的执行效率。并且我们可以帮助我们控制线程的数量、复用、优先级、销毁等。可以帮助我们减少因为线程频繁创建和销毁的开销。
2、线程池的基本信息介绍
2.1 核心参数
- 核心线程数:corePoolsize
- 最大线程数:maximumPoolSize
- 任务队列 :BlockingQueue workQueue
- ArrayBlockingQueue:一个用数组实现的有界阻塞队列,按照FIFO原则对元素进行排序。支持公平锁、非公平锁
- LinkedBlockingQueue:一个用链表实现的有界阻塞队列,按照FIFO原则对元素进行排序。队列默认长度为Integer.MAX_VALUE,所以使用该队列有风险
- DelayQueue:无界队列,在添加元素时可以指定时间,时间到后才可以从队列获取元素
- PriorityBlockingQueue:支持优先级排序的无界队列。默认自然序排列,也可以自定义实现compareTo()指定排序规则
- LinkedBlockingDeque:链表结构组成的双向阻塞队列,队列头尾都可以添加移除元素。
- SynchronousQueue:不存储元素的阻塞队列,每一次put操作必须等待task操作,否则不能添加元素。
- 线程存活时间:keepAliveTime
- 存活时间单位:TimeUnit unit,可以选时分秒等
- 创建工厂:ThreadFactory
- 拒绝策略:RejectedExecutionHandler
2.2 线程池状态:
状态 | 描述 |
---|---|
RUNNING | 线程池正常运行中,可以正常接受并处理任务 |
SHUTDOWN | 线程池被关闭,不可以接受新任务,但是会把阻塞队列中剩余要处理的任务执行完成,执行完成后会中断所有工作线程 |
STOP | 线程池关闭,不能接受新的任务,并且也不会处理阻塞队列中的任务,中断所有工作线程 |
TIDYING | 所有工作线程都会中断后会进入这个状态 |
TERMINATED | 处于TIDYING状态时,会执行terminted()方法,执行完后会进入这个状态,但是在ThreadPoolExecute中这个方法是空方法,但是可以自定义线程池重写这个方法 |
3、ThreadPoolExecutor
因为在阿里巴巴开发规范中,是不建议通过Executors来创建线程池的,因为使用不当的话容易造成OOM(原文下面有介绍)。
我们通过都是通过创建TreadPoolExecutor来使用线程池,本质上是构建了一个生产者-消费者模型,让线程和任务达到一个解耦的状态。ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务
3.1 使用线程池的简单demo
public class ThreadPoolExecutorTest {
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
1, //核心线程数
1, //最大线程数
10, //非核心线程存活时间
TimeUnit.SECONDS, //非核心线程存活时间单位
new ArrayBlockingQueue<>(5), //任务队列
new ThreadPoolExecutor.AbortPolicy()//拒绝策略
);
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
//没有返回值使用execute
THREAD_POOL_EXECUTOR.execute(()->{
System.out.println(getRandomInt());
});
//有返回值使用submit
Future<?> submit = THREAD_POOL_EXECUTOR.submit(() -> {
System.out.println(getRandomInt());
});
//会阻塞获取返回值
try {
Object o = submit.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
private static int getRandomInt() {
Random random = new Random();
return random.nextInt(200);
}
}
3.3 源码解析
3.3.1 excute方法源码分析
在解析源码前我们需要只要一些源码中使用元素的基本信息以及代表什么
public class ThreadPoolExecutor extends AbstractExecutorService {
private static int ctlOf(int rs, int wc) {
return rs | wc;
}
//workerCount 有效线程数
//runState 表示状态,是否运行等
//线程池的状态和线程数
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// CAPACITY就是当前工作线程能记录的工作线程的最大个数
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
/*
* 线程池c状态小于s状态,例如running<shutdown
*/
private static boolean runStateLessThan(int c, int s) {
return c < s;
}
/*
* 线程池c状态大于于s状态,例如stop>shutdown
*/
private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}
/*
* 线程池状态是否为running
*/
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
/*
*通过CAS操作 增加工作线程个数
*/
private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}
/*
*通过CAS操作 减少工作线程个数
*/
private boolean compareAndDecrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect - 1);
}
/*
* AtomicInteger 操作,底层也是CAS进行加减
*/
private void decrementWorkerCount() {
ctl.addAndGet(-1);
}
}
excute方法
//当我们调用execute进行任务执行时
THREAD_POOL_EXECUTOR.execute(()->{
//业务代码
})
public void execute(Runnable command) {
//任务为空则直接抛异常
if (command == null)
throw new NullPointerException();
/*
在ThreadPoolExecutor中,firstTask是在初始化worker类时传入的Runnable对象。当构造worker对象时,需要传入firstTask,这个worker将要运行这个Runnable对象作为它的第一个任务。所以,firstTask实际上是线程池中某个工作线程(worker)开始执行时的第一个任务,而不是与核心线程数直接对应的任务
*/
//获取线程数量和当前状态
int c = ctl.get();
//如果线程数量小于核心线程数,则调用addWorker把当前任务当作首个任务firstTask
//并在下游服务通过核心线程数和状态来判断是否添加成功
if (workerCountOf(c) < corePoolSize) {
//执行添加工作线程后,下游会继续判断线程数
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//任务状态并且队列添加成功,然乎获取线程数量,如果当前状态不是running且从队列中remove
// 任务成功,则执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
//如果线程个数为0,但是已经添加任务成功了,所以添加一个非核心线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果以非核心线程数添加工作线程失败,则可能是线程数>最大线程数,则直接执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
3.3.2 addWorker添加线程方法源码分析
- 会控制一下工作线程数,例如是否小于corePoolSize 以及maximumPoolSize
- 以及判断线程池状态。是否为running状态,来决定是否添加工作线程
- 并且把执行任务添加到workers队列中
- 如果添加任务成功但是线程start失败了,则会对线程进行回收addWorkerFailed
//worker的构造函数,并且通过AQS实现
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
//获取线程数量和当前状态
for (int c = ctl.get();;) {
/**
runStateAtLeast检查状态是否为指定状态SHUTDOWN
且状态为stop,firstTask不为空,队列为空,则直接返回创建任务失败
**/
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP)
|| firstTask != null
|| workQueue.isEmpty()))
return false;
for (;;) {
//判断线程数量是否大于等于核心线程数/最大线程数
//添加核心线程数时会传入core = true,反之则为false
if (workerCountOf(c)
>= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
return false;
//如果不是则把ctl中线程数+1,结束这次循环
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
//再次判断线程池状态
if (runStateAtLeast(c, SHUTDOWN))
continue retry;
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//创建一个新的worker,并取出线程
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
//确保线程安全地访问worker集合和其他共享资源。
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int c = ctl.get();
//再次检查状态,确保此时线程池状态不是关闭
//状态为Running或者状态为SHUTDOWN且队列中任务为空
if (isRunning(c) ||
(runStateLessThan(c, STOP) && firstTask == null)) {
//如果此时线程状态不是新建的,则证明可能其他地方用到了,则抛出异常
if (t.getState() != Thread.State.NEW)
throw new IllegalThreadStateException();
//然后把新创建的worker添加到workers中
workers.add(w);
workerAdded = true;
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//则启动线程
t.start();
//启动线程成功
workerStarted = true;
}
}
} finally {
//如果添加的worked没有启动成功,则调用addWorkerFailed来处理失败的情况
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
//如果线程没有启动成功
private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (w != null)
//从workers集合中移除
workers.remove(w);
//并且更新ctl线程数量,-1
decrementWorkerCount();
tryTerminate();
} finally {
mainLock.unlock();
}
}
3.3.3 runWorker执行任务方法源码分析
线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用
在利用ThreadFactory创建线程时,会把this,也就是当前Work对象作为Runnable传给线程,所以工 作线程运行时,就会执行Worker的run方法:
public void run() {
// 这个方法就是工作线程运行时的执行逻辑
runWorker(this);
}
- 循环执行任务
- 判断一下当前线程是否有第一执行任务,如果没有则从阻塞队列中获取
- 当线程池状态为STOP时,可以中断
- 可以在任务执行、执行后处理
- 会捕获异常,并且抛出供执行者执行和处理,最后由processWorkerExit方法判断是否异常原因来处理
- 执行完后会记录一下完成任务数
- 然后把任务进行销毁
final void runWorker(Worker w) {
//获取当前线程
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
//清除添加时设置的第一任务,表示当前任务可以执行
w.firstTask = null;
w.unlock();
//表示任务是否是发生异常退出的
boolean completedAbruptly = true;
try {
//如果任务不为空或者从阻塞队列中getTask()方法获取不为空,则一直循环执行
while (task != null || (task = getTask()) != null) {
w.lock();
//判断线程池状态是否为stop ,要确保线程中断
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
//如果状态为stop且线程的中断标记为true则把当前线程中断
wt.interrupt();
try {
//执行前处理
beforeExecute(wt, task);
try {
task.run();
//执行后处理
afterExecute(task, null);
} catch (Throwable ex) {
afterExecute(task, ex);
throw ex;
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
//没异常则设置为false
completedAbruptly = false;
} finally {
//非核心线程不能一直存活,删除线程的方法
processWorkerExit(w, completedAbruptly);
}
}
3.3.4 getTask获取任务方法源码分析
- 检查线程池状态
- 控制线程数量,如果线程数大于核心线程数且超时未获取到任务,则会减少线程数
- 循环获取,直到没有合适的任务
- 从队列中通过pool、take获取任务
- 如果尝试获取被中断则会捕获InterruptedException,并重置超时标记,然后继续循环尝试获取任务
private Runnable getTask() {
//判断是否因为超时而没有获取到任务
boolean timedOut = false;
for (;;) {
int c = ctl.get();
//状态为stop,状态是否为shutDown且队列为空)
//表示当前线程不需要处理任务了,则直接返回null
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
//减少线程数量
decrementWorkerCount();
//返回null,代表没有任务执行
return null;
}
int wc = workerCountOf(c);
//allowCoreThreadTimeOut为true,表示线程池中的所有线程都可以被回收掉
//则当前线程应该直接使用超时阻塞,一旦超时就回收
//是否允许当前活动线程数超过核心线程数,则要看当前工作线程数是否超过了corePoolSize
//如果超过了,则表示超过部分的线程要用超时阻塞,一旦超时就回收
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
//如果超过了核心线程数且超时,或者队列为空但是线程数大于1
//则线程数进行相减
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
//如果允许核心线程超时或当前活动线程数超过核心线程数,则超时获取任务
//如果不是则阻塞获取任务
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
//获取任务为空,则返回任务
if (r != null)
return r;
//为true则代表超时获取任务失败了,这时我们就要判断如果队列为空,但是线程数不为空,则返回bull,结束循环
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
3.3.5 processWorkerExit方法源码分析
- 调整工作线程数
- 更新任务计数器并且移除工作线程
- 检查是否需要添加新的工作线程,如果需要则添加
private void processWorkerExit(Worker w, boolean completedAbruptly) {
//如果工作线程是发生异常而退出的,那么工作线程数还没有被调整
if (completedAbruptly)
//减少工作线程
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//任务累计加到线程池中
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}
tryTerminate();
int c = ctl.get();
//如果当前线程状态为RUNNIG或者SHUTDOWN
if (runStateLessThan(c, STOP)) {
//则证明是正常退出的
if (!completedAbruptly) {
// 如果allowCoreThreadTimeOut为true,但是阻塞队列中还有任务,那就 至少得保留一个工作线程来处理阻塞队列中的任务
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
// 如果allowCoreThreadTimeOut为false,那min就是corePoolSize,表示 至少得保留corePoolSize个工作线程活着
//如果线程池中线程数量大于min,则直接返回
if (workerCountOf(c) >= min)
return; //
}
// 如果线程池的状态为RUNNING或者SHUTDOWN
// 如果completedAbruptly为true,表示当前线程是执行任务时抛了异常,那就得新开一个非核心工作线程
// 如果completedAbruptly为false,证明队列中任务不为空且不符合所需要保留的最小线程数,,那也得新开一个非核心工作线程
addWorker(null, false);
}
}
3.3.6 mainLock
在上述源码中,发现很多地方都会用到mainLock,它是线程池中的一把全局锁,主要是用来控制 workers集合的并发安全,因为如果没有这把全局锁,就有可能多个线程公用同一个线程池对象,如果一个线程在向线程池提交任务,一个线程在shutdown线程池,如果不做并发控制,那就有可能线程池 shutdown了,但是还有工作线程没有被中断,如果1个线程在shutdown,99个线程在提交任务,那么最终就可能导致线程池关闭了,但是线程池中的很多线程都没有停止,仍然在运行,这肯定是不 行,所以需要这把全局锁来对workers集合的操作进行并发安全控制。
4、使用注意事项
- 在阿里巴巴使用规约中,尽量不要使用Executors工具来创建线程池,如果使用不当的话容易导致OOM。
线程池名称 | 构造函数 | 分析 |
---|---|---|
newFixedThreadPool | new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,newLinkedBlockingQueue()); | 核心线程数和最大线程数一样,但是他使用的队列是LinkedBlockingQueue,但是它的默认值为Integer.MAX_VALUE。如果造成任务堆积,则可能发生OOM |
newSingleThreadExecutor | new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()) | 核心线程数和最大线程数数量都为1,但是他使用的队列是LinkedBlockingQueue,但是它的默认值为Integer.MAX_VALUE。如果造成任务堆积,则可能发生OOM |
newCachedThreadPool | new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue()) | 核心线程数为0,最大线程数为Integer.MAX_VALUE,容易造成OOM |
ScheduledThreadPoolExecuto | public ScheduledThreadPoolExecutor(int corePoolSize) {super(corePoolSize, Integer.MAX_VALUE,DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue());} | 最大线程数为Integer.MAX_VALUE,容易造成OOM |
- 核心线程数、最大线程数、队列
但是现如今不管是I/O密集型或者是CPU密集型,业界都没有很成熟的技术规范,大部分都是根据开发者自己经验来配置。这三个参数的设置是最核心的,如果最大线程数配置过小,如果接口遇到突发流量,导致接口访问出现大量的RejectedExecutionException。如果队列使用不当,配置的数量过大,这样会导致最大线程数失效,导致大量任务堆积到队列中,导致调用接口发生超时。 - 可以对核心参数放到nacos中,然后对线程池状态进行监控,然后根据实际情况可以实现动态配置
总结
以上就是分享的线程池的一些概念和源码分析,希望能让大家对线程池有一些简单的认识。