ThreadPoolExecute执行流程状态流转
Posted 苦咖啡-coffe
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadPoolExecute执行流程状态流转相关的知识,希望对你有一定的参考价值。
线程池相关知识点大家可能都了解核心线程数、最大线程数、任务阻塞队列、拒绝策略等这些,也知道这些参数具体使用,但如果你没详细看过源码的话对很多实现细节还是不了解,比如Worker类创建时为何要赋值state=-1,Worker类执行任务前为何要解锁、执行时为何要加锁,业务任务异常未捕获则线程池会怎么处理,shutdown和shutdownnow实现方式区别是什么,下面带着这些问题学习ThreadPoolExecute线程池组件实现源码。
一、线程池状态流转
1.1 线程池状态
// runState is stored in the high-order bits
// 高位存储111
private static final int RUNNING = -1 << COUNT_BITS;
// 高位存储000
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 高位存储001
private static final int STOP = 1 << COUNT_BITS;
// 高位存储010
private static final int TIDYING = 2 << COUNT_BITS;
// 高位存储011
private static final int TERMINATED = 3 << COUNT_BITS;
1.2 线程池状态流转
二、任务执行流程
线程池组件中线程任务执行流程分析,以及状态流转。
基于线程池组件任务执行流程分析各个特殊节点源码,上面流程中只有execute入口而没有submit是因为我们分析的是线程池组件而不是线程池中执行的任务,所以从这个角度来看execute和submit是一样的实现逻辑,所以只分析一个入口流程。
下面分析线程池组件中任务执行中特殊的几个点,也就是文章开头我们提到的几个点,这几个点我在上图流程中做了特殊标记,接下来分析具体源码实现。
2.1 ThreadPoolExecute 对象创建
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ThreadPoolExecute对象创建,线程池状态为Running、工作线程个数0。
2.2 Worker类对象创建
2.2.1 Worker继承关系
private final class Worker extends AbstractQueuedSynchronizer implements Runnable
从类的继承关系看Worker类继承了AQS抽象类,所以它具备锁功能,实现Runnable接口又是一个线程任务。作为一个工作线程为何要实现AQS,为何要实现Runnable接口,带着这两个问题继续。
2.2.2 Worker实例成员变量
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
Thread 实例成员变量就是线程池中新建的工作线程,后续的任务执行都是被该线程驱动执行。而Runnable 成员变量为应用中要执行的任务,按照上面注释看他是该线程第一个任务,有可能为空,如果为空则会从任务阻塞队列获取任务执行。
2.2.3 Worker类构造函数
Worker(Runnable firstTask)
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
构造函数中有两个看点
第一setState(-1);Worker类继承了AQS类所以这里是将锁状态赋值为-1,按理说默认是0,加锁也是判断是否为0来判断是否能获得锁,这里为何赋值为-1。按照注释解释是在runWorker之前禁止中断,那他是怎么防止线程被中断的。ThreadPoolExecute.Worker 类虽然继承了AQS但重写了加锁方法,Worker类中不支持锁而且加锁是cas(0,1),如果是-1 则加锁失败。当调用shutdown方法时会通过Worker.tryLock()方法加锁,只有加锁成功后才能中断工作线程。而创建Worker对象时默认state=-1 所以这时调用shutdown中断线程肯定是加锁失败,所以无法中断。
第二使用线程工厂时传递参数为当前Worker类对象,也就是说Worker类会作为线程Thread的任务。那就明白了Worker类为何要实现Runnable接口。
2.2.4 Worker任务类run方法中执行runWorker方法
final void runWorker(Worker w)
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
//1设置状态state=0
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try
while (task != null || (task = getTask()) != null)
//2执行任务前加锁
w.lock();
//状态判断省略……
try
beforeExecute(wt, task);
Throwable thrown = null;
try
//3业务任务执行
task.run();
catch (RuntimeException x)
//省略异常处理……
finally
task = null;
w.completedTasks++;
w.unlock();
completedAbruptly = false;
finally
processWorkerExit(w, completedAbruptly);
2.2.5 Worker类对象unlock()方法
Worker类的run方法中会调用runWorker方法,而这个方法中通过自旋方式执行任务。但是在方法执行前会调用Worker.unlock() 方法,该方法是是干嘛用的。
public void unlock() release(1);
protected boolean tryRelease(int unused)
setExclusiveOwnerThread(null);
setState(0);
return true;
看代码中变量命名“未使用”,代码中确实没使用直接赋值state=0,这样Worker锁恢复到初始状态,这时可以被中断了。也就是说当我们添加一个任务时只有该任务启动线程并开始执行任务时才允许被中断。为何只有设置为state=0该工作线程才能被中断,这个在后面的shutdown和shutdownNow方法中细说。
2.2.6 Worker类对象lock()方法
protected boolean tryAcquire(int unused)
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
return true;
return false;
当通过自旋方式执行任务时首先要获得锁才能执行任务,否则会被添加到AQS的双向列表等待锁,但是Worker是一个工作线程,要么是执行创建是指定的Runnable任务,要么是执行从阻塞队列中获取的任务,没有线程安全问题为何还要加锁。其实刚才说了shutdown方法中断线程时也要获取锁,否则没法执行中断,也就是说shutdown方法中没法中断执行任务中的线程。还有Worker类中的锁和ReentrantLock是有区别的,Worker类中锁不具备重入功能。
2.3 总结
2.3.1 Worker类继承并重写了加锁方法,所以Worker类不具备重入锁功能,只能一次加锁、解锁。
2.3.2 Worker类是一个执行任务的线程,他为何还要继承AQS实现锁功能,发现在shutdown方法和runWorker方法中都使用锁,用来限制执行任务中工作线程不会被标记为中断。而shutdownNow方法中就没有这个限制。
2.3.3 Worker类初始化state=-1,是该工作线程被标记为中断的一个限制,只有执行了runWorker方法的工作线程才能标记为中断。
2.3.4 Worker类中加锁方式区分线程是否在执行任务,如果state=0则空闲,state=1则在执行业务任务。
2.3.5 Worker类的加锁有两个疑问,第一shutdown中为何只是中断空闲线程,如果和shutdownNow方法一样都被标记为中断会有什么问题。第二个问题未执行runWorker方法前线程被标记为中断会存在什么问题。
三、线程池关闭即shutdown、shutdownNow
shutdown方法关闭线程池,也是将线程池从Running状态转为为shutdown,当线程池处于shutdown状态时,线程池不再接收新添加的任务,而已经在线程池阻塞队列中的任务会被执行完,shutdownNow方法停止线程池,不仅不接收受业务任务而且线程池任务阻塞队列中的任务也会被移除,不会被执行,下面分析两个方法具体实现细节。
3.1 shutdown方法
public void shutdown()
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try
checkShutdownAccess();
//1更新线程池状态为shutdown
advanceRunState(SHUTDOWN);
//2将空闲工作线程设置为中断
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
finally
mainLock.unlock();
tryTerminate();
标注1只是将线程池状态设置为shutdown,没有特殊操作。 标注2中有加锁操作,这里和runWorker方法中Worker类开始执行任务时获取锁类似具体看代码。
3.1.1 interruptIdleWorkers
将空闲工作线程标记为中断
private void interruptIdleWorkers()
interruptIdleWorkers(false);
3.1.2 interruptIdleWorkers
private void interruptIdleWorkers(boolean onlyOne)
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try
for (Worker w : workers)
Thread t = w.thread;
//1工作线程没有被标记为中断并且是空闲的。
if (!t.isInterrupted() && w.tryLock())
try
//2工作线程被标记为中断
t.interrupt();
catch (SecurityException ignore)
finally
//3释放锁
w.unlock();
//4注意这里的break,参数为false
if (onlyOne)
break;
finally
mainLock.unlock();
标注1中判断线程是否已标记为中断,如果否并且工作线程为空闲则将该线程标记为中断。为何说线程为空闲,在runWorker方法中看到当执行任务时会获取锁,任务执行完后会释放锁,所以这里能获取锁说明该线程处于空闲状态。也证明了工作中线程不会被标记为中断。
标注4,因为参数为false所以不会只中断一个工作线程就退出而是遍历所有工作线程,只要该线程空闲则都会被标记为中断。而空闲线程要么阻塞在任务队列中要么刚释放,分析一下这种场景下怎么结束工作线程。
场景一:任务队列为空,任务线程被阻塞在任务队列中
这种场景下,该工作线程已经释放了锁,所以shutdown方法中会将该工作线程标记为interrupt,这时阻塞队列中会抛出InterruputException,结束该线程阻塞。
private Runnable getTask()
boolean timedOut = false; // Did the last poll() time out?
//1自旋方式获取任务队列中任务
for (;;)
int c = ctl.get();
int rs = runStateOf(c);
//2如果线程池为shutdown并且任务队列为空则返回null
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty()))
decrementWorkerCount();
return null;
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 省略工作线程大于核心线程数情况……
try
//3工作线程被阻塞这里,如果标记为中断则抛InterruptedExcepiton
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
catch (InterruptedException retry)
timedOut = false;
看标记1、3如果任务队列为空则直接返回null,这样runWorker方法自旋结束,驱动该方法的Worker线程任务执行完成,更新工作线程个数,从HashSet<Worker>集合删除该工作线程,线程生命周期结束。线程池工作线程数减一。
场景二:工作队列中存在任务
看上面代码,如果队列不为空还是会返回具体任务,正常执行任务队列中任务。
3.2 shutdownNow方法
public List<Runnable> shutdownNow()
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try
checkShutdownAccess();
//1更新线程池状态为stop
advanceRunState(STOP);
//2中断线程
interruptWorkers();
//3将任务队列中任务移动到tasks队列中
tasks = drainQueue();
finally
mainLock.unlock();
tryTerminate();
return tasks;
shutdownNow方法相比shutdown方法不仅状态更新为stop而且标记2中中断方法也不同,并且标记3中将任务队列中任务都移除到临时变量tasks中。
3.2.1 interruptWorkers方法
private void interruptWorkers()
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try
for (Worker w : workers)
w.interruptIfStarted();
finally
mainLock.unlock();
这个方法相比shutdown方法中中断方法,不用获取锁,意味着即使是工作中线程也会被标记为中断即所有工作线程都会被标记为中断。
3.2.2interruptIfStarted方法
void interruptIfStarted()
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted())
try
t.interrupt();
catch (SecurityException ignore)
只要该工作线程被启动了或者说runWorker方法中调用了unlock方法则都会被标记为中断。
3.2.3 drainQueue方法
将任务队列中任务移到tasks局部变量中并返回给shutdownNow方法调用方。
3.2.4 runWorker中getTask方法
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty()))
decrementWorkerCount();
return null;
当前线程状态为stop并且任务队列任务为空所以返回null给runWorker方法,工作线程自旋结束,这样runWorker中执行从HashSet<Worker>删除工作线程,线程方法结束,线程结束。
3.3 总结
shutdown、shutdownNow方法特性对比
3.3.1 调用shutdown、shutdownNow方法都会将线程池状态从Running变更为shutdown或stop,这两种状态下都会有新任务添加到任务队列。当调用execute方法执行任务时会直接调用reject(Runnable command) 方法执行拒绝策略类。
3.3.2 shutdown状态下如果任务阻塞队列中有任务则工作线程会将这些任务执行完,因为shutddown方法中没有操作任务阻塞队列workQueue,而shutdownNow方法被调用后会将任务阻塞队列中任务移除。这也就是大家说的shutdown会将任务度列中任务执行完而shutdownNow不会继续执行任务度列中任务实现方式。
3.3.3 shutdown方法中只对空闲工作线程做中断标记而shutdownNow中对所有工作线程都做中断标记。当任务队列中无任务时都会结束该工作线程,从核心线程数、任务执行这些场景下没发现有什么区别。
这篇文章太长了,任务执行异常分析后面文章继续!
开发者涨薪指南 48位大咖的思考法则、工作方式、逻辑体系以上是关于ThreadPoolExecute执行流程状态流转的主要内容,如果未能解决你的问题,请参考以下文章