Java线程池学习
线程是操作系统的调度和分配的基本单位,所以想要优化高并发系统的工程师少不了需要跟线程打交道,故此,学习线程的工作原理对于Java工程师是必不可少的进阶之路。
在学习线程之前,需要先想清楚自己学习线程应该到哪个程度才算满意?我打算通过几个问题来学习和校验自己的线程学习程度。
- 线程的生命周期?
- 如何复用线程?
- Java线程池的核心属性有哪些?其主要作用有哪些?
- 线程池新建线程的逻辑(有界队列、无界队列)?
- 线程池的拒绝策略?
- 如何监控线程池?
- 线程池状态的设计(相关位运算学习)?
文章目录
线程的生命周期
在操作系统层面,线程包含五个状态,分别是:新建、就绪、运行、阻塞、结束。
而Java语言定义了另外五种线程状态:新建、运行、无限期等待、限期等待、阻塞、结束。其中Java的运行状态包括了操作系统层面的就绪与运行状态,Java的无限期等待、限期等待、阻塞为操作系统层面的阻塞状态。
新建
通过new关键字新建一个线程时,该线程就是新建状态,此时仅由JVM分配内存和初始化其成员对象,当新建线程调用start()
方法,则线程进入就绪状态。
就绪
处于就绪状态的线程JVM会为其创建方法调用栈和程序计数器,处于就绪状态的线程并没有被cpu调用执行,而是处于等待cpu调度的状态。线程何时转为运行状态,取决于操作系统。
处于就绪状态的线程不能在调用start()
方法,否则会报IllegaIThreadStateExccption
异常。
线程进入就绪状态有这些情况:
- 新建线程调用
start()
方法 - 解除阻塞状态的线程
- 运行状态的线程调用
yield()
方法
Java线程调度
线程调度指系统为线程分配处理器使用权的过程,主要调度分为两种方式:协同式线程调度和抢占式线程调度,Java使用的线程调度方式是抢占式调度,但不确保某些极早的虚拟机使用协同式线程调度。
协同式线程调度
协同式调度的多线程系统中,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。
- 好处是实现简单,而且由于线程要把自己的事情做完后才会进行线程切换,切换操作对线程自己是可知的,所以不存在线程同步问题。
- 缺点是执行时间不可控,如果线程编写有问题,一直不通知系统进行线程切换,那么程序就会一直阻塞着。
抢占式线程调度
抢占式调度的多线程系统中,每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(仅可通过yield()
释放cpu资源,但是获得cpu资源不可控)。
虽然Java提供了设置优先级的方法setPriority(int newPriority)
,但是Java线程优先级并不靠谱,原因是Java的线程是通过映射到系统的原生线程上实现的,所以线程调度最终还是取决于操作系统,然而不同系统提供的优先级数却不定相同,例如Solaris中有2的32次方种优先级,而Windows只有7种,这就意味着Windows中优先级不能完全对应Java优先级;而且有些系统会自行改变优先级,这些情况都告诉我们不能太相信设置优先级带来的效果。
运行
就绪状态的线程获得cpu调度就会进入运行状态,此时开始执行run()
方法的线程执行体,如果计算机只有一个cpu,则任何时刻只有一个线程处于运行状态,如果是对处理器的计算机,就可以同时有多个线程并行执行,当线程数大于机器处理器数时,则会存在多个线程在一个cpu上切换的现象,这也是不建议线程池中的核心线程数设置超过处理器数两倍的原因。
阻塞
运行状态的线程如果发生以下情况,会转为阻塞状态:
- 调用
sleep()
主动放弃占有的cpu资源,直到休眠时间结束由系统唤醒线程进入就绪状态 - 调用了阻塞式IO操作,在返回方法之前,该线程被阻塞,直到IO操作结束,线程进入就绪状态
- 试图获得同步监视器,但是该同步监视器被其他线程所拥有,知道成功获得同步监视器进入就绪状态
- 等待某个通知,直到获得其他线程发出的通知进入就绪状态
- 调用
suspend()
挂起,直到调用resdme()
恢复,该方法容易造成死锁,已被废弃
结束
运行线程遇到如下情况会转为结束状态:
run()
和call()
方法运行结束,线程正常结束- 线程抛出一个未捕获异常或error
- 调用
stop()
主动结束线程,该方法容易造成死锁,已被废弃。
已经结束的线程不能在通过start()
方法试图重新唤起,这样会报否则会报IllegaIThreadStateExccption
异常。
如何复用线程?
要学习复用线程池,就应该从源码入手。
首先,我们从执行任务代码处开始看起
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
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);
}
else if (!addWorker(command, false))
reject(command);
}
很明显看出,线程池执行任务有四种情况:
- 如果正在运行的线程少于
corePoolSize
线程,使用传入的runnable
作为其第一个任务来启动一个新线程; - 当线程数等于核心线程数且任务队列不满时,任务入队列,复用线程去任务队列中拿任务执行;
- 当任务队列已满时且线程数小于最大线程数,创建新线程;当任务无法入队且线程创建失败时,拒绝服务。
具体线程怎么新增线程的,再看addwork()
代码
private boolean addWorker(Runnable firstTask, boolean core) {
// 方便学习,此处省略大量条件判断代码
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类
封装了线程的实例过程,当线程创建成功后,将线程放进Workers队列
中,接着启动线程;如果创建失败则Workers队列
移除该线程。
看到这里大概知道了线程的启动过程,但是线程的是如何创建的?如何将用户任务作为第一个任务?这些问题还没有得到很好的解答,带着这些问题可以继续看Woker类
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
可以看到Woker类
的构造函数,发现线程是通过ThreadFactory
创建,并把firstTask
传给newThread
.
直到现在代码也看了上百行,虽然知道了线程池创建线程的原理了,但是线程池的线程是如何复用的仍然半知半解,线程在完成第一个任务后为什么不会销毁,而是会去任务队列中找下一个任务的呢?答案就在眼前,我们再接着往下看
/** Delegates main run loop to outer runWorker */
public void run() {
runWorker(this);
}
Woker类的run方法,它调用了runWorker()
方法,继续刨根问底
/**
* Main worker run loop. Repeatedly gets tasks from queue and
* executes them, while coping with a number of issues:
*
* 1. We may start out with an initial task, in which case we
* don't need to get the first one. Otherwise, as long as pool is
* running, we get tasks from getTask. If it returns null then the
* worker exits due to changed pool state or configuration
* parameters. Other exits result from exception throws in
* external code, in which case completedAbruptly holds, which
* usually leads processWorkerExit to replace this thread.
*
* 2. Before running any task, the lock is acquired to prevent
* other pool interrupts while the task is executing, and then we
* ensure that unless pool is stopping, this thread does not have
* its interrupt set.
*
* 3. Each task run is preceded by a call to beforeExecute, which
* might throw an exception, in which case we cause thread to die
* (breaking loop with completedAbruptly true) without processing
* the task.
*
* 4. Assuming beforeExecute completes normally, we run the task,
* gathering any of its thrown exceptions to send to afterExecute.
* We separately handle RuntimeException, Error (both of which the
* specs guarantee that we trap) and arbitrary Throwables.
* Because we cannot rethrow Throwables within Runnable.run, we
* wrap them within Errors on the way out (to the thread's
* UncaughtExceptionHandler). Any thrown exception also
* conservatively causes thread to die.
*
* 5. After task.run completes, we call afterExecute, which may
* also throw an exception, which will also cause thread to
* die. According to JLS Sec 14.20, this exception is the one that
* will be in effect even if task.run throws.
*
* The net effect of the exception mechanics is that afterExecute
* and the thread's UncaughtExceptionHandler have as accurate
* information as we can provide about any problems encountered by
* user code.
*
* @param w the worker
*/
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;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
为了更好理解,我把原码注释也放进来,再用我那不堪入目的英语水平简单翻译一下:
线程不断循环从任务队列中拿任务并执行,同时还解决一些问题:
- 如果线程已有第一个任务,就不需要从pool中拿,如果线程试图拿任务但是返回
null
,则修改线程池状态或配置参数然后退出,否则其他退出情况是外部代码异常引起的。 - 如果线程池正在运行任务,将获取锁以防止任务执行期间线程池被中断,并且除非线程池正在停止,否则线程不能设置其中断。
- 每个任务运行之前都会调用
beforeExecute
,如果引起异常,为了防止破坏循环,我们将销毁该线程而不是继续处理任务。 - 假设
beforeExecute
正常完成,线程将执行任务,并收集执行任务期间引发的任何异常以发送给afterExecute
。而且我们分别处理RuntimeException
、Error
(只要规格符合捕捉)和任意Throwables
。因为线程不能在Runnable.run
中重新抛出Throwables
,所以将异常包装在线程的UncaughtExceptionHandler
的Errors中。任何抛出的异常也会导致线程死亡。 task.run
完成后,线程会调用afterExecute
,这可能还会引发异常,也会导致线程死亡。
可以看出,线程池为了保证自身的循环复用线程机制,宁愿霸道的结束在执行过程中出现异常的线程也不愿意回收它们,以引发不可知的错误导致循环破坏。
再上面看原码
while (task != null || (task = getTask()) != null)
可以看出,线程在不断循环从任务队列中getTask()拿任务执行
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;
}
}
}
看完这段代码,终于知道了拿任务也不是那么简单的
- 如果线程数超过了最大线程数,只会返回null
- 线程等待超时也拿不到任务
拿不到任务的后果就是被清理了,很现实啊
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}
tryTerminate();
int c = ctl.get();
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
addWorker(null, false);
}
}
看到这里,我们已经清楚了线程池的线程复用原理了,在回顾一下:
首先,线程池启动,用户调用线程池执行task任务,此时会出现3种情况:
- 线程数小于核心线程数
- 线程数等于核心线程数且任务成功入队列
- 任务入队失败,尝试创建新线程
线程创建出来后,就会不断循环从任务队列中拿任务,当任务队列为空时,核心线程也会根据keepAliveTime
处于活跃状态,继续不断循环拿任务,而超过核心线程数的线程在等待完第一个keepAliveTime
后死亡。
Java线程池的核心属性有哪些?其主要作用有哪些?
线程池的属性有七个,分别是核心线程数corePoolSize
,最大线程数maximumPoolSize
,存活时间keepAliveTime
,存活时间单位unit
,任务队列workQueue
,线程工厂threadFactory
,拒绝策略handler
。
下面是ThreadPoolExecutor
类的构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize
-核心线程数:线程池空闲时间仍可保留的线程数maximumPoolSize
-最大线程数:线程池最大线程数keepAliveTime
-存活时间:超过核心线程数的线程在空闲超过存活时间就会死亡unit
-时间单位:keepAliveTime的时间单位workQueue
-任务队列:当线程池没有空闲线程时,任务进入任务队列等待空闲线程获取执行threadFactory
-线程创建工厂:线程创建工程,建议自定义该属性,方便给线程定义名称打印日志handler
-拒绝策略:当线程池没有空闲线程且任务进入任务队列失败时,线程池拒绝执行任务的操作
线程池新建线程的逻辑(有界队列、无界队列)
- 有界队列:当有新的任务需要执行,如果线程池线程数小于核心线程数,则创建新的线程执行任务;如果线程数等于核心线程数,则任务进入任务队列等待空闲线程执行;如果任务队列已满,则判断线程数小于最大线程数,创建新的线程执行任务,如果线程数等于最大线程数,则执行拒绝策略。
- 无界队列:
LinkedBlockQuery
。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在如对失败的情况。当有任务需要执行,线程池的线程数小于核心线程数,则新建线程执行任务;当线程数等于核心线程数,则不增加线程,后续执行任务进入任务队列中等待,若任务创建和处理的速度差异很大,无界队列会保持快速增长,知道资源耗尽为止。
线程池的拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize
,如果还有任务到来就会采取任务拒绝策略。
ThreadPoolExecutor.AbortPolicy
:丢弃任务,并抛出RejectedExecutionException
异常。ThreadPoolExecutor.CallerRunsPolicy
:该任务被线程池拒绝,由调用execute
方法的线程执行该任务(通常为主线程);如果执行程序已关闭,则会丢弃该任务。ThreadPoolExecutor.DiscardOldestPolicy
: 抛弃队列最前面的任务,然后重新尝试执行任务。ThreadPoolExecutor.DiscardPolicy
:丢弃任务,不过也不抛出异常。
如何监控线程池
ThreadPoolExecutor
提供的API方法如下:
1.getPoolSize()
:初始线程数
2. getCorePoolSize()
:核心线程数
3. getActiveCount()
:正在执行的任务数量
4. getCompletedTaskCount()
:已完成任务数量
5. getTaskCount()
:任务总数
6. getQueue().size()
队列里缓存的任务数量
7. getLargestPoolSize()
:池中存在的最大线程数
8. getMaximumPoolSize()
:最大允许的线程数
9. getKeepAliveTime(TimeUnit.MILLISECONDS)
:线程空闲时间
10. isShutdown()
:线程池是否关闭
11. isTerminated()
:线程池是否终止
同时,ThreadPoolExecutor
类预留给开发者进行扩展的方法:
12. shutdown()
:线程池延迟关闭时(等待线程池里的任务都执行完毕)
13. shutdownNow()
:线程池立即关闭
14. beforeExecute(Thread t, Runnable r)
:任务执行之前执行该方法
15. afterExecute(Runnable r, Throwable t)
:任务执行之后执行该方法
线程池状态的设计(相关位运算学习)
线程池状态设计涉及到了二进制的位运算,Doug Lea
大佬巧妙的将线程的状态与线程数用一个int数解决,不得不让人钦佩一番。
关于线程池的位运算源码先贴上来,再来分析:
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;
// 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; }
通过源码,不难发现,线程池使用一个 int 类型来存储 线程池中当前的线程数量 与 状态。
其设计思想是:用 int 的高3位用来表示线程池的状态,int 的低29位用来表示当前线程池中的线程数量。
COUNT_BITS
: 这个参数就是用来表示用于表示线程个数所占用的位数,这里等于 29。
CAPACITY
: 线程池中线程的总个数,有了位运算的基本知识后,这个不难理解,00011111 11111111 11111111 11111111
为了方便查看,我将线程池五种状态的二进制作成下面的表格
status | Binary |
---|---|
RUNNING | 1110 0000 0000 0000 0000 0000 0000 0000 |
SHUTDOWN | 0000 0000 0000 0000 0000 0000 0000 0000 |
STOP | 0010 0000 0000 0000 0000 0000 0000 0000 |
TIDYING | 0100 0000 0000 0000 0000 0000 0000 0000 |
TERMINATED | 0110 0000 0000 0000 0000 0000 0000 0000 |
上面状态通过向左移动29位,可以保证这些状态的低29位都为0。
线程池使用变量 AtomicInteger ctl 来表示状态 与线程数量,其实现方法为:
private static int ctlOf(int rs, int wc) { return rs | wc; }
就是将 状态 与 线程数 做 | 运算,其实好理解。
上面的状态,低 29位都是 000,而用于表示线程数量的int ,高3位都为0,而 利用 a | 0 = a 的特性实现位的相互不影响。
那如何从一个 ctl 变量中快速得到当前的状态呢?
那如何从 ctl 中提取出状态呢?
private static int runStateOf(int c) { return c & ~CAPACITY; }
其实就是要提前 int 的高3 位置,通常的套路是使用 & 运算,参与与运算的另外一个操作数,高3为都是111,低29位置都是0,这样就能提前3位的 111 ,而不受低29位的1的影响,因为低29位都是0。故使用了 ~ CAPACITY,其中 CAPACITY 的值为:00011111 11111111 11111111 11111111
,取反为 11100000 00000000 00000000 00000000
。
同样,如果要从 ctl 中 提取线程数量,则使用如下表达式。
private static int workerCountOf(int c) { return c & CAPACITY; }
好啦,以上就是我阅读线程池源码时的一些学习心得体会,阅读源码越深,对于技术的敬畏就更多,学习永无止步。