java线程池想要搞明白的一些问题
在高并发场景,往往需要同时执行多个任务,如果每个任务新创建一个线程,任务执行完后在销毁线程,那么CPU会有大量的资源消耗在线程的创建和销毁上,并且系统的资源是有限的,不能一直创建线程。这个时候大家一般都会采用线程池来解决线程重复创建和销毁的过程,
- 每次任务来了从线程池找一个空闲的线程来执行任务,
- 如果没有空闲的线程就把任务放到队列等待,
- 如果等待队列满了,就执行拒绝策略(如丢弃任务)。
虽然上面的概念都很熟悉了,但是线程池是如何管理线程资源并把任务分派给线程执行,线程执行任务的入口在哪里,线程的状态转换和线程池的状态转换这些问题一直都是一知半解。本文通过分析Java Thread 和ThreadPoolExecutor源码,尝试着对上面困扰我的几个问题做一个浅显的回答。
线程的6种状态的转换:
java中所有任务的执行都离不开线程Thread,Thread共有6种状态,这里仅表示JVM层面的线程状态,不表示操作系统层面的线程状态。
(图片转载自blog.csdn.net/pange1991/a…)
-
New
新建一个线程还没开始运行(即还没有调用start方法,注意调用run方法并不会新启一个线程)
-
RUNNABLE
调用start方法后,线程进入runnable状态,runnable状态表示可运行状态,最初我觉得这里应该细分就绪(ready)状态和运行中(running)状态,看了Thread源码后发现对此有说明。处于runnable状态的线程在JVM中执行,但是可能在等待一些操作系统层面的资源,如处理器、硬盘等,因此这个RUNNABLE状态是包括了ready状态和running状态的。
这里可能会有个疑问,线程等待操作系统层面的资源不就是block或者wait了吗?注意JVM里面的线程操作系统层面的block和wait与操作系统不同,操作系统上为等待CPU,硬盘等资源而阻塞,但是JVM层面线程仍在运行中,线程仍处于runnable状态。JVM层面的block,wait指的是同步资源快的等待(可以简单理解为等待锁释放)。 -
BLOCKED
BLOCKED状态有2种情况,当线程首次进入同步代码块时需要等待获取监控锁,此时处于BLOCKED状态,线程在同步代码块内调用Object.wait方法后会释放锁并进入等待状态,处于等待状态的线程收到Object.notify或Object.notifyAll通知后,不能立即恢复执行,想要进入同步代码块时需要获取锁,然后从上次wait的地方恢复执行,此时也处于BLOCKED状态。这2种情况都可以归结于线程没有获得锁导致无法进入同步代码块时处于blocked状态。
-
WAITING
调用Object.wait(with notimeout)Thread.join(with notimeout),LockSupport.park等方法后线程进入WAITING状态。一个线程调用某个对象的wait方法后会等待另一个线程调用该对象的notify或notifyAll方法,调用了Thread.join方法会等待一个特定线程结束。WAITING状态的线程与BLOCKED状态的线程区别在于WAITING状态的线程不再活动,也不会去竞争锁了。join的场景是在a线程中调用b.join,此时a会等待b线程执行完再执行,b执行完后系统会隐式通知a,a收到通知后恢复执行。
-
TIMED_WAITING
和WAITING相比有一个等待时间,超出等待时间后会自动唤醒
-
TERMINATED
线程的run方法执行完后就终止了
线程的运行
线程被创建后处于new状态,调用start方法后处于runnable状态(包括ready和running,要看线程是否获得操作系统的资源),线程只能调用一次start方法,从源码中可以看出,如果线程的状态不是0(new 状态)调用start方法会抛出异常。调用start方法后实际会再另启动一个线程,本线程执行start方法,start方法会调用native start0方法,start0方法会启动一个新线程然后JVM会在start0方法内执行run方法(需要获取到操作系统资源才开始执行),这时候任务真正的开始被跑。run方法只是一个普通方法,线程任何时候都可以调用。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
@Override
public void run() {
if (target != null) {
target.run();
}
}
线程池的状态
JAVA线程池设计者用AtomicInteger类型变量 ctl来表示线程池的线程数量和状态并限制了线程的数量为(2^29)-1(因为简单并且设计者认为目前足够用,不够用了再改成AtomicLong),高三位(30-32)用来表示线程池状态。线程池有以下几种状态
- RUNNING:接受新任务和队列中任务,用-1表示。
- SHUTDOWN:不接受新任务但会执行完队列中任务,用0表示。
- STOP:不接受新任务,不接受队列中任务,并且会打断执行中任务,用1表示。
- TIDYING:所有任务结束并且workercount数量为0,线程池过渡到TIDYING状态并执行terminated方法,用2表示。
- TERMINATED:terminated方法执行完后的状态,用3表示。
public class ThreadPoolExecutor extends AbstractExecutorService {
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
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;
看看runStateOf(表示线程池状态)和workerCountOf(线程数量)这2个方法,初始状态下,ctl的值为11100000 ... … 00000000,表示 RUNNING 状态,和0个工作线程,每创建一个线程,ctl的值加1,在 RUNNING 状态下,ctl 始终是负值,而 SHUTDOWN 是0,所以可以通过直接比较 ctl 的值来确定线程池状态。
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
线程池的创建方式
最直接的方式通过构造函数来创建线程池。构造方法共7个参数,在创建线程池时需要根据系统的资源以及业务场景来组装合适的参数。比如最大QPS为100,每个任务的处理时间为100ms,那么核心线程数和线程总数设置为10可以每秒执行完100个任务,但是考虑到特殊情况导致的任务异常或者超时等,可以把线程总数调的大一点。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- 核心线程数,线程池优先创建核心线程。相比非核心线程,核心线程空闲时不会丢弃
- 线程总数,线程池最多能创建的线程数 = 核心线程数+非核心线程数
- 保活时间,非核心线程空闲的最长时间,超过则会销毁非核心线程
- 时间单位
- 队列,当有新任务且线程总数已满,会把任务加到等待队列知道有新的线程空闲后从队列取出任务执行
- threadFactory:线程工厂,用来创建新线程时给线程取名字
- handler:拒绝策略,当无法再增加线程数时,或拒绝添加任务时,所执行的策略
另一种创建线程池的方式是使用Executors类,Executors把一些常见的场景封装好了,其实就是通过组装构造函数的方式封装。根据阻塞队列的不同可以分为2类线程池,ThreadPoolExecutor(使用的3种阻塞队列)和ScheduleThreadPoolExecutor(使用的一种阻塞队列)
- FixedThreadPool:拥有固定数量线程的线程池,限制了线程的数目,适用于负载比较重的服务器。阻塞队列为LinkedBlockingQueue,核心线程数和线程数都为n
- SingleThreadPool:单个线程的线程池,适用于任务保证顺序执行。阻塞队列为LinkedBlockingQueue ,核心线程数和线程数都为1
- CachedThreadPool:大小无界的线程池,适用于负载较轻的服务器。阻塞队列为SynchronousQueue,核心线程数0,线程数Inter.MAX_VALUE
ScheduleThreadPoolExecutor类:
- ScheduleThreadPoolExecutor:包含多个线程支持周期任务的线程池。DelayedWorkQueue
- SingleThreadScheduleExecutor: 只包含单个线程支持周期任务的线程池。DelayedWorkQueue
线程池的线程如何重复利用
使用线程池的一个好处是线程可以重复使用,避免线程大量的新建和销毁浪费资源,那么线程是如何重复利用的呢?我们知道所有任务的执行,无论是单个线程还是线程池最终都是通过Executor接口的execute方法来执行任务。线程池ThreadPoolExecutor实现了这个接口,execute方法的实现分3步:
public interface Executor {
void execute(Runnable command);
}
-
获取ctl变量,判断是否小于核心线程总数,小于则创建核心线程来执行任务addWorker(command, true)
- 创建核心线程成功,执行任务
- 创建失败则再次获取ctl变量。并执行步骤2
-
检查线程池状态为running并且把任务插入队列(2个条件只要有一个不满足则执行步骤3)
- 首先再次获取ctl变量double-check下线程池是否处于running状态,不属于running则不应该添加新任务只需要执行完队列剩余的任务
- 如果线程池不在running状态,首先从队列移除该任务,移除成功执行拒绝策略,移除失败进入2.c
- 线程池在running状态或者从队列移除任务失败,首先判断线程池中是否存在线程,不存在会创建非核心线程来执行之前已经添加到等待队列的任务addWorker(null, false),执行完后线程池会真正关闭。注意addWorker并没有指定任务而是null。
-
step2如果插入队列失败,或者线程池不处于running状态,创建非核心线程执行队列中剩余的任务。非核心线程创建失败的话则直接拒绝任务。
public void execute(Runnable command) {
int c = ctl.get();
//step1
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//step2
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//step3
else if (!addWorker(command, false))
reject(command);
}
从上面的execute方法可以看出,不管创建核心线程执行任务还是非核心线程执行任务都是在执行addWorker方法,看下addWorker方法做了什么。他有两个参数Runnable firstTask 表示要执行的任务,boolean core 表示要添加的是核心线程还是非核心线程。
- retry循环,主要用来判断一些addWorker不能添加的情况比如连接池状态大于SHUTDOWN(不能添加任务且不能执行队列里面的任务),或者活动线程数超过阈值。
- 首先实例化一个worker,设置这个worker要执行的第一个任务并且创建一个线程t给到这个worker去执行任务
- 判断线程池状态为running或者shutdown(此时需要判断firstTask ==null。
- 检查线程t 是否是isAlive状态,如果线程是isAlive 状态说明线程已经执行过start方法了并且线程还没有死忙,此时抛出线程状态异常。
- 线程t执行start方法,执行start方法会执行worker的run方法,而run方法又执行了runWorker(this),runWorker最终会执行任务。
- 设置worker状态为workerStarted,说明这个worker已经开始执行任务
private boolean addWorker(Runnable firstTask, boolean core) {
//省略部分代码,一个retry循环
//以下为添加addWorker逻辑
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
//实例化worker
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
再看下runWorker方法,首先会去拿到worker w里面的任务由于一开始实例化时给worker任务赋值了,所以task!=null ,因此会执行task.run(终于看到了线程是怎么执行的!),任务执行完后把task置空,这时while循环判断第二个条件(task = getTask()) != null。如果getTask返回不为空,说明一直有任务要执行,那runWorker将一直循环,getTask方法本身是个死循环,里面有3种情况。
- 线程池状态大于等于stop或者(线程池状态大于等于shutdwon且队列为空),此时减少线程并且getTask方法返回空
- 一般情况下allowCoreThreadTimeOut默认为false,只有wc大于核心线程数timed才会是true。当前活跃线程数若大于线程池最大值,或者(time和timedout都为true),以上2个条件满足一个并且(wc > 1 || workQueue.isEmpty()),则会回收多余线程并且getTask方法返回null,如果回收线程失败则继续循环。
- 以上2个条件都不满足或者(wc > 1 || workQueue.isEmpty())不满足时,getTassk方法会尝试获取任务,timed为true时,队列会等待一个超时时间,超时时间内有任务则getTask方法返回任务,超时时间内没任务则把timedout置为true并进行下一次循环,timed为false时一直等待直到有任务。 结合上述三种情况,只要有任务时候,线程池内的线程就回一直循环执行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 {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
//最终执行任务的地方
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
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.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
```