线程池
- 使用线程池的好处
- 线程池工作原理
- 创建线程池
- 线程池执行任务
- 线程池生命周期管理
- worker
JAVA 中的线程池是应用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在阿里巴巴《java 开发手册》中甚至说明线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
。足以显示线程池的重要性。
使用线程池的好处
-
降低资源消耗
通过重复利用已创建的线程降低线程创建和销毁造成的消耗。对操作系统来说,创建一个线程的代价是十分昂贵的, 需要给它分配内存、列入调度,同时在线程切换的时候还要执行内存换页,CPU 的缓存被清空,切换回来的时候还要重新从内存中读取信息,破坏了数据的局部性。
-
提高响应速度
任务到达时,任务可以不需要等待线程创建完毕。
-
提高线程的可管理性
线程是稀缺资源。如果无限制的创建。不仅会消耗系统资源。还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
线程池工作原理
其运行机制如下图(图片来自文末美团技术文章)
使用者调用 execute、submit 提交任务到线程池。线程池有四步来处理这个任务
- 先看看能不能创建核心线程,即便有核心线程空闲,但是核心线程数没到核心线程数的最大值,还是选择新建核心线程来处理
- 如果核心线程数满了,就加入任务队列中。
- 队列也满了,但是线程总数(线程总数 >= 核心线程数)没有达到限制,就创建新的工作线程来处理任务
- 任务是在是太多了,线程数已经到达最大线程数了,执行饱和策略,如:丢弃
线程池execute源码如下
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) { // 1.创建核心线程
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) { // 2.核心线程满了,试试看能不能扔进队列里
int recheck = ctl.get(); // 扔进队列里再重复检查一下
if (! isRunning(recheck) && remove(command)) // 线程池关闭了,那队列里的任务也执行不到了,直接拒绝任务
reject(command);
else if (workerCountOf(recheck) == 0) // 工作线程刚好全销毁了,起一个非核心线程把队列里的任务消费一下
addWorker(null, false);
}
else if (!addWorker(command, false)) // 3. 队列添加失败,试试看创建非核心线程
reject(command); // 4. 创建非核心线程失败,那就只能执行饱和策略了
}
创建线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
-
corePoolSize :核心线程数量
-
maximumPoolSize :所有线程的数量
-
keepAliveTime 、 unit : 空闲线程的存活时间,当线程数 > corePoolSize 并且该线程空闲了 keepAliveTime 时间后,就会销毁,直到线程数 == corePoolSize
-
BlockingQueue :任务阻塞队列,多个线程对队列进行操作时会阻塞,保证并发安全。
常见的阻塞队列有
ArrayBlockingQueue、LinkedBlockingQueue
。ArrayBlockingQueue、LinkedBlockingQueue 顾名思义一个是用数组结构,一个是链表结构,值得一提的是两者实现线程安全的原理都是使用了ReentrantLock,但是通常情况下 LinkedBlockingQueue 的吞吐量要更高,因为 ABQ 使用一个全局锁,而LBQ 是链表结构添加任务到队尾和拿队列头的任务是独立的两个操作,所以使用了两个锁,生产锁和消费锁。
SynchronousQueue
: 不存储元素的队列,每一个生成操作需要另一个线程调用消费操作,否则一直处于阻塞状态PriorityBlockingQueue
:具有优先级的无限阻塞队列 -
ThreadFactory :通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
-
RejectedExecutionHandler : 饱和策略,当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。AbortPolicy 为默认执行策略。
- AbortPolicy:直接抛出异常,会 throws RejectedExecutionException
- CallerRunsPolicy:调用者所在线程自己来运行任务
- 其把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列
- DiscardPolicy:不处理,丢弃掉
线程池的顶层接口和设计为Executor,里面只定义了一个execute 方法。
public interface Executor {
void execute(Runnable command);
}
所有的线程池都实现这个接口,Executor 将任务的提交和执行分离开来,用户只需要通过execute 方法来提交任务,后续的工作由实现了Executor 类的各种线程池实现类来对任务进行执行。
ThreadPoolExecutor 就是Executor 的核心类,依据这个类,传入不同的参数,可以得到不同特性的线程池啦满足各种业务需求。甚至实现动态线程池。
JDK中也提供了3种不同特性的线程池:
-
FixedThreadPool : corePoolSize 和 MaximumPoolSize 都被设置为一个指定的参数,并且将线程空闲时间设置为0。队列设置为LBQ无界队列。线程池完成预热之后,线程数稳定,队列无限,处理任务速度基本不变。
-
SingleThreadPool 使用单个线程:corePoolSize 和 MaximumPoolSize 都被设置为1,其他都和FixedThreadPool 一样。
-
CachedThreadPool: corePoolSize 为0,MaximumPoolSize 为 integer 最大值。线程空闲存活时间为 60s。队列为无容量的 SynchronousQueue。如果主线程提交任务速度很快,那么 CachedThreadPool 会不断创建新线程。很有可能会耗尽CPU和内存资源。
主线程提交任务时是直接提交给 SynchronousQueue 的,由于该队列会阻塞住主线程的提交,直到有线程执行poll来将该任务取走执行。如果线程池空着,那么会创建一个线程来拿。线程执行完之后,也会执行poll操作等待主线程提供任务。如果60s内都没有拿到任务,那么该线程就会被终止。
阿里巴巴《java开发手册》中不允许使用Executors 创建上述的模板线程池
Executors 创建的线程中使用了无界队列、integer 最大值的线程数量这些无限制的限制条件。
LinkedBlockingQueue阻塞队列。当核心线程用完后,任务会入队到阻塞队列,如果任务执行的时间比较长,没有释放,会导致越来越多的任务堆积到阻塞队列,最后导致机器的内存使用不停的飙升,造成JVM OOM。
不允许使用无界队列。
线程池执行任务
一共有两种方法提交任务:
- execute( )
- submit( )
两者区别:
- execute只能提交Runnable类型的任务,无返回值。submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。
- execute在执行任务时,如果遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法(阻塞) 获取返回值时,才会抛出异常。
多线程异步
Future的get方法是阻塞的,对于结果的获取,不是很友好,只能通过阻塞或者轮询的方式得到任务的结果。Future提供了一个isDone方法,可以在程序中轮询这个方法查询执行结果。
阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源。
因此,JDK8设计出CompletableFuture
。CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。
CompletableFuture 可以传入自定义线程池,实现异步调用。如果不传入自定义线程池,也有默认的线程池。
CompletableFuture 创建异步任务的两个方法:
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) // 无返回值
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) // 有返回值
线程池生命周期管理
线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量ctl 维护两个值:运行状态(runState)和线程数量 (workerCount)。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
- ctl (线程池控制状态)是原子整型的,这意味这对它进行的操作具有原子性。
- 同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。同时具有了原子性。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。
ctl 使用了一系列位运算操作,将这两个状态打包为一个变量和分别拿出对应的值。如:
1 //拆包函数
2 private static int runStateOf(int c) { return c & ~CAPACITY; }
3 private static int workerCountOf(int c) { return c & CAPACITY; }
4 //打包函数
5 private static int ctlOf(int rs, int wc) { return rs | wc; }
线程池调用shutdown( ) 方法:将线程池状态变为 SHUTDOWN
,中断所有没有正在执行的线程,就是调用线程的interrupt方法来中断线程,但是这块仅仅是调用中断方法,将中断标志 interrupted 置为true 并不意味着线程就会终止,直到该线程执行的任务中有方法如:acquireInterruptibly
会响应中断。
线程池调用shutdownNow( )
方法 : 将线程池状态变为 stop
, 尝试终止所有正在执行的线程,和上面一样也只是遍历线程然后调用线程的interrupt方法。
线程生命周期转换如下图所示(图片来自文末美团技术文章)
worker
线程池将每一个线程和他拿到的任务包装为一个 Worker 内部类, 使用 completedTasks 记录执行完的任务数量。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
}
public void run() {
runWorker(this);
}
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
worker
继承了AQS同步器,自己本身实现了锁的功能。自己使用了lock、tryLock()、unlock()、isLocked() 方法使用aqs实现锁的功能,相当于实现lock接口的方式。
将锁的粒度细化到每个工Worker,如果多个Worker使用同一个锁,那么一个Worker Running持有锁的时候,其他Worker就无法执行,这显然是不合理的。
什么时候将线程和任务封装为worker对象的?
用户使用线程池的时候就是执行execute或者submit方法将任务丢进去,那就要从execute 方法看起。execute方法里面的各种判断逻辑已经很清楚了,就是线程池工作原理。execute里面调用addWorker( ) 将任务对象继续往下传递。addWorker()顾名思义就是将我们的任务对象和线程对象放入worker对象中。
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
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;
}
addWorker 源码总结一下就是:先自旋cas修改线程数量,修改成功了再去拿到线程池的全局锁,将新创建的worker 放入 Set 容器中。成功之后将worker线程开启。
注意worker 这里的 run 方法,调用了线程池的runWorker,并且将自己作为参数传进去,很容易想到runWorker 就是将worker里面的任务给执行了。先看看自己的任务有没有,再看看getTask()看看队列里有没有任务需要处理, 然后获取worker对象对应的锁,成功之后终于可以执行 task.run() 将任务执行掉。
需要注意的是这里设置了2个钩子函数:
- beforeExecute(wt, task)
- afterExecute(task, thrown)
这两个函数方法体为空,我们可以创建ThreadPoolExecutor的子类来重写beforeExecute(Thread, Runnable)方法,使得线程池正式执行任务之前,执行我们自己定义的业务逻辑。
processWorkerExit(Worker, boolean)方法的逻辑主要是执行退出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);
}
}
学习链接: