登录/注册
小企鹅
2678
占位
0
占位
0
浏览量
占位
粉丝
占位
关注
细说JUC的线程池架构
小企鹅
2021-11-27 11:53:59 2021-11-27
78
0

前言

线程的创建是需要JVM和OS(操作系统)相互配合的,一次的创建要花费许多的资源。

1.首先,JVM要为该线程分配堆栈和初始化大量内存块,栈内存至少是1MB。

2.其次便是要进行系统的调用,在OS中创建和注册本地的线程。

在Java的高并发场景下频繁的创建和销毁线程,一方面是内存块的频繁分配和回收,另一方面是操作系统频繁注册线程和销毁,内存资源利用率不高的同时,也增加了时间的成本,这是非常低效的。我们要做的是在线程执行完用户代码逻辑块后,保存该线程,等待下一次用户代码逻辑块来到时,继续去运用该线程去完成这个任务,这样不仅减少了线程频繁的创建和销毁,同时提高了性能。具体的实现便是线程池技术。

JUC线程池架构

线程池技术主要来自于java.util.concurrent包(俗称JUC),该包是JDK1.5以后引进来的,主要是完成高并发,多线程的一个工具包。

线程池主要解决了线程的调度,维护,创建等问题,它在提高了线程的利用率的同时还提高了性能。

在JUC中,有关线程池的类和接口大致如图下所示:

接下来让我们一个一个解析每个接口和类吧。

Exector接口

我们从源码来看Exector

public interface Executor {
void execute(Runnable command);
}

我们可以看到Exector只有一个接口方法便是execute()方法,该方法是定义是线程在未来的某个时间执行给定的目标执行任务,也就是说线程池中的线程执行目标任务的方法。

ExecutorService接口

该接口继承了Executor因此它也继承了execute()方法,同时它本身也扩展了一些重要的接口方法,我们通过源码看一下几个比较常用的方法

public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
}
  • shutdown()接口方法的定义是关闭线程池,它与shutdownNow()方法不同的点在于,它不会中止正在执行的线程,它也会把未完成的目标任务完成了,此时线程池的状态未SHUTDOWN,执行回调函数后关闭线程池。
  • shutdownNow()接口方法的定义也是关闭线程池,它与shutdown()方法不同的点在于,它会中止正在执行的线程,清空已提交但未执行的目标任务,返回已完成的目标任务,同时线程池的状态为STOP,执行回调函数后关闭线程池。
  • isShutdown()接口方法的定义是判断当前的线程池状态是否是SHUTDOWN状态,是返回true,不是返回false。
  • isTerminated()接口方法的定义是判断当前线程池状态是否是TERMINATED状态,也就是判断当前线程池是否已经关闭,不是返回flase,是返回true。
  • submit()接口方法的定义与execute()方法类似,也是提交目标任务给线程池,线程池中的线程在适合的时机去执行该目标任务,它与execute()方法不同的点在两个:一方面是submit()方法的形参可以有是Callable类型的,也可以是Runnable类型的,而execute()方法仅能接收Runnable类型的,另一方面是submit()方法的返回值类型是Future,这意味着,我们可以获取到目标任务的执行结果,以及任务的是否执行、是否取消等情况,而execute()方法的返回值是void类型,这也表示我们获取不到目标任务的执行情况等信息。

AbstractExecutorService抽象类

正如第一张图显示的:AbstractExecutorService抽象类继承了ExecutorService接口,这意味着AbstractExecutorService抽象类拥有着父类ExecutorService接口的所有接口方法,同时因为ExecutorService接口又继承了Executor接口,因此也拥有Executor接口的接口方法。

不仅如此AbstractExecutorService抽象类实现了除shutdown()、shutdownNow()、execute()、isShutdown()、isTerminated()以外的方法,这里我们主要查看一下submit()方法的实现,同时也可以加深我们对execute()和submit()的关系与区别。

public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}

我们来解读一下源码:

  1. 首先判断传进来的Runnable类型的对象是否为空,如果为空的话便抛出一个空指针异常。
  2. 若不为空,则将目前的Runnable类型对象传入newTaskFor方法,我们走进newTaskFor方法可以发现,其将Runnable类型对象修饰成了一个FutureTask类型的对象,FutureTask是实现了RunnableFuture接口的实现类,因此可以将其赋予给ftask。
  3. 随后,调用了execute方法,将ftask作为目标任务,传入线程池,等待线程池调度线程执行这个任务,最后再返回ftask,便于调用线程监控目标任务的执行情况和执行结果。

从源码分析我们可以得知,submit()方法本质上还是调用了executor()方法,只不过将Runnable类型的对象修饰成了FutureTask类型,让其拥有监控执行任务的能力而已。

有关Callable接口和FutureTask实现类以及RunnableFuture接口的详细信息可以查阅笔者另一篇随笔: https://www.cnblogs.com/qzlzzz/p

ThreadPoolExecutor线程池实现类

ThreadPoolExecutor继承了AbstractExecutorService抽象类,因此也就拥有了AbstractExecutorService抽象类继承的实现了的接口方法和AbstractExecutorService抽象类所继承的未实现的接口方法。

在此前提下,ThreadPoolExecutor不仅实现了AbstractExecutorService抽象类未实现的接口方法,同时其内部真正的实现了一个线程池,且实现了线程池的调度,管理,维护,空闲线程的存活时间,默认的线程工厂,和阻塞队列,核心线程数,最大线程数,淘汰策略等功能。我们也可得知ThreadPoolExecutor是线程池技术的核心、重要类。"由于本随笔仅说JUC的线程池架构,因此不多描述线程池的实现等其核心功能"

这里我们着眼于ThreadPoolExecutor对execute()方法的实现,首先我们来看其源码:

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);
}

其实在源码中已经有很详细的注释解析了,甚至不追及都可以懂这段代码的作用,我想这就是好的程序员的一种体现,笔者在追寻源码的路上时候也慢慢体会到尤雨溪大佬为什么那么强调:想要水平提升,必须有好的英语。

接着我们来大致说一下源码:

1. 首先会判断传入进来的Runnable类型的对象是否为空,如果为空则抛出一个空指针异常。

2. 随后获取ctl的值,ctl是原子类,在定义时它的初始值是 -536870912,获取到值后赋给c变量,c变量传入workerCountOf()方法,在方法的内部进行了或运算,以此来获取线程池的线程数,如果线程池的线程数比定义线程池时所设置的核心线程数要少的话,不管线程池里的线程是否空闲,都会新建一个线程。

3. 判断为true的话,进入到嵌套if()中的addWorker()方法。

这里我们再来探寻一下addWorker()方法的源码:

private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (int c = ctl.get();;) {
// Check if queue empty only if necessary.
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP)
|| firstTask != null
|| workQueue.isEmpty()))
return false;
for (;;) {
if (workerCountOf(c)
>= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateAtLeast(c, SHUTDOWN))
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 c = ctl.get();
if (isRunning(c) ||
(runStateLessThan(c, STOP) && firstTask == null)) {
if (t.getState() != Thread.State.NEW)
throw new IllegalThreadStateException();
workers.add(w);
workerAdded = true;
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

在addWorker()方法里面我们可以看到充斥着大量线程池在SHUTDOWN和STOP状态时,线程池该怎样去运作,当线程池中的线程数达到核心线程数,线程池又如何去做,以及如何选取空余的线程去执行目标任务或者在阻塞队列中的目标任务等调度,创建功能。

在如此长的一段代码中我们关注这几行:

//第一段代码
w = new Worker(firstTask);
final Thread t = w.thread;

以及

//第二段代码
if (workerAdded) {
t.start();
workerStarted = true;
}

首先是第一段代码,我们可以看到它将目标任务Runnable类型的对象修饰成了Worker类型,我们翻看一下Worker类:

private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable{
//省略其他代码
Runnable firstTask;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
//省略其他代码
}

从Worker的构造方法中我们可以知道其将目标任务,传给了自己的实例属性,同时由于自己本身是Runnable的实现类,因此可以以自己本身作为参数传入到线程工厂的构造线程方法中,而自己本身实现的run()方法中又调用了runWorker()方法,runWorker()方法的参数又是当前Worker的实例本身,如果读者有意深入的话,会发现runWorker()方法体中有一段是task.run()去执行目标任务,其余的代码则是回调函数的调用。

也就是说线程工厂创建的线程,如果启动该线程去执行的话,是执行Worker类中的run()方法,也就会去执行run()方法中的runWorker()方法。

然后我们继续来看第一段代码,其使用句柄w获取到thread,赋予给了Thread类型的t变量。第一段代码结束,再到第二段代码中使用了t.start()来启动这个线程去执行目标任务,再将这个任务的工作状态设为ture。

至此,两段代码探讨结束。

4. 最后回到execute()方法中,继续走下去便是一些线程池拒绝策略的判断,在这里就不过多叙述了。

ScheduledExecutorSerive接口

从关系图可以得知ScheduledExecutorService接口继承了ExecutorService接口,这说明ScheduledExecutorService接口拥有着ExecutorService接口的接口方法,同时除了ExecutorService的提交、执行、判断线程池状态等的接口方法之外,ScheduledExecutorService接口还拓展了一些接口方法。

这里我们从接口定义中来解读ScheduledExecutorService究竟增加了那些功能。

public interface ScheduledExecutorService extends ExecutorService {
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
  • 可以看出schedule()有两种重载方法,区别在于第一种方法接收的形参是Runnable类型的,第二种方法接收的形参是Callable类型的。其作用都是前一次执行结束到下一次执行开始的时间是delay,单位是unit。
  • scheduleAtFixedRate()接口方法的定义是首次执行目标任务的时间延迟initialDelay,两个目标任务开始执行最小间隔时间是delay,其单位都是unit。
  • scheduleWithFixedDelay()接口方法的定义与schedule()方法类似,只不过是首次执行的时间延迟initialDelay,单位是unit。

    注意上面的方法都是周期的,也就是说会周期地执行目标任务。

ScheduledThreadPoolExecutor线程池实现类

ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,同时实现了ScheduledExecutorSerive接口,这意味着ScheduledThreadPoolExecuotr不仅拥有了ThreadPoolExecutor实现的线程池,同ScheduledExecutorService接口继承的接口方法也无需其实现,因为ThreadPoolExecutor和AbstractExecutorService已经帮其实现了。在此基础上,ScheduledThreadPoolExecutor实现了ScheduledExecutorService接口拓展的方法,这使得ScheduledThreadPoolExecutor成为一个执行"延时"和"周期性"任务的可调度线程池。

至此,JUC线程池架构也逐渐清晰了起来,Exector接口定义了最重要的execute方法,ExecutorService 则拓展了提交和执行的方法,也扩展了监控线程池的方法、AbstractExecutorService抽象类则负责实现了ExecutorService 接口拓展的方法,因为ThreadPoolExecutor类内部实现了线程池,所以监控线程池的方法和execute方法等重要的方法自然也交给了其实现。最后的ScheduledThreadPoolExecuto类其实也是在整个完整的线程池技术上,拓展了线程池的一些功能。

结尾

一定要吃早餐,学习的过程需注意自己的身体才行。

原文: https://www.cnblogs.com/qzlzzz/p/15369636.html

暂无评论