线程池

简介: 线程池通过复用线程提升性能,避免频繁创建销毁的开销。Java中由Executor框架实现,核心为ThreadPoolExecutor,管理线程生命周期与任务调度。通过Executors工厂创建,支持提交异步任务、定时执行等。关键组件包括工作队列、线程工厂与拒绝策略,实现高效并发控制。(238字)

一、线程池初探
所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务。线程池的关键在于它为我们管理了多个线程,我们不需要关心如何创建线程,我们只需要关系我们的核心业务,然后需要线程来执行任务的时候从线程池中获取线程。任务执行完之后线程不会被销毁,而是会被重新放到池子里面,等待机会去执行任务。
我们为什么需要线程池呢?首先一点是线程池为我们提高了一种简易的多线程编程方案,我们不需要投入太多的精力去管理多个线程,线程池会自动帮我们管理好,它知道什么时候该做什么事情,我们只要在需要的时候去获取就可以了。其次,我们使用线程池很大程度上归咎于创建和销毁线程的代价是非常昂贵的,甚至我们创建和销毁线程的资源要比我们实际执行的任务所花费的时间还要长,这显然是不科学也是不合理的,而且如果没有一个合理的管理者,可能会出现创建了过多的线程的情况,也就是在JVM中存活的线程过多,而存活着的线程也是需要销毁资源的,另外一点,过多的线程可能会造成线程过度切换的尴尬境地。
对线程池有了一个初步的认识之后,我们来看看如何使用线程池。

  1. 创建一个线程池

ExecutorService executorService = Executors.newFixedThreadPool(1);

  1. 提交任务

executorService.submit(() -> System.out.println("run"));
Future stringFuture = executorService.submit(() -> "run");

  1. 创建一个调度线程池

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

  1. 提交一个周期性执行的任务

scheduledExecutorService
.scheduleAtFixedRate(() -> System.out.println("schedule"), 0, 1, TimeUnit.SECONDS);

  1. shutdown

executorService.shutdownNow();
scheduledExecutorService.shutdownNow();

可以发现使用线程池非常简单,只需要极少的代码就可以创建出我们需要的线程池,然后将我们的任务提交到线程池中去。我们只需要在结束之时记得关闭线程池就可以了。本文的重点并非在于如何使用线程池,而是试图剖析线程池的实现,比如一个调度线程池是怎么实现的?是靠什么实现的?为什么能这样实现等等问题。
二、Java线程池实现架构
Java中与线程池相关的类有下面一些:
● Executor
● ExecutorService
● ScheduledExecutorService
● ThreadPoolExecutor
● ScheduledThreadPoolExecutor
● Executors
通过上面一节中的使用示例,可以发现Executors类是一个创建线程池的有用的类,事实上,Executors类的角色也就是创建线程池,它是一个工厂类,可以产生不同类型的线程池,而Executor是线程池的鼻祖类,它有两个子类是ExecutorService和ScheduledExecutorService,而ThreadPoolExecutor和ScheduledThreadPoolExecutor则是真正的线程池,我们的任务将被这两个类交由其所管理者的线程池运行,可以发现,ScheduledThreadPoolExecutor是一个集大成者类,下面我们可以看看它的类关系图:

ScheduledThreadPoolExecutor的类关系图

ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,ThreadPoolExecutor实现了一般的线程池,没有调度功能,而ScheduledThreadPoolExecutor继承了ThreadPoolExecutor的实现,然后增加了调度功能。
ScheduledThreadPoolExecutor相较于ThreadPoolExecutor增加了调度功能
最为原始的Executor只有一个方法execute,它接受一个Runnable类型的参数,意思是使用线程池来执行这个Runnable,可以发现Executor不提供有返回值的任务。ExecutorService继承了Executor,并且极大的增强了Executor的功能,不仅支持有返回值的任务执行,而且还有很多十分有用的方法来为你提供服务。

Executor不提供有返回值的任务,ExecutorService继承自Executor,支持有返回值的任务执行

下面展示了ExecutorService提供的方法:

ExecutorService提供的方法

ScheduledExecutorService继承了ExecutorService,并且增加了特有的调度(schedule)功能。关于Executor、ExecutorService和ScheduledExecutorService的关系,可以见下图:

Executor、ExecutorService和ScheduledExecutorService的关系
总结一下,经过我们的调研,可以发现其实对于我们编写多线程代码来说,最为核心的是Executors类,根据我们是需要ExecutorService类型的线程池还是ScheduledExecutorService类型的线程池调用相应的工厂方法就可以了,而ExecutorService的实现表现在ThreadPoolExecutor上,ScheduledExecutorService的实现则表现在ScheduledThreadPoolExecutor上,下文将分别剖析这两者,尝试弄清楚线程池的原理。
三、ThreadPoolExecutor解析
上文中描述了Java中线程池相关的架构,了解了这些内容其实我们就可以使用java的线程池为我们工作了,使用其提供的线程池我们可以很方便的写出高质量的多线程代码,本节将分析ThreadPoolExecutor的实现,来探索线程池的运行原理。下面的图片展示了ThreadPoolExecutor的类图:

ThreadPoolExecutor的类图
下面是几个比较关键的类成员:

// 任务队列,我们的任务会添加到该队列里面,线程将从该队列获取任务来执行
private final BlockingQueue workQueue;

//任务的执行值集合,来消费workQueue里面的任务
private final HashSet workers = new HashSet();

//线程工厂
private volatile ThreadFactory threadFactory;

//拒绝策略,默认会抛出异异常,还要其他几种拒绝策略如下:
private volatile RejectedExecutionHandler handler;

1、CallerRunsPolicy:在调用者线程里面运行该任务
2、DiscardPolicy:丢弃任务
3、DiscardOldestPolicy:丢弃workQueue的头部任务

//最下保活work数量
private volatile int corePoolSize;

//work上限
private volatile int maximumPoolSize;

我们尝试执行submit方法,下面是执行的关键路径,总结起来就是:如果Worker数量还没达到上限则继续创建,否则提交任务到workQueue,然后让worker来调度运行任务。

step 1:
Future<?> submit(Runnable task);

step 2:<AbstractExecutorService>
    public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

step 3:<Executor>
void execute(Runnable command);

step 4:<ThreadPoolExecutor>
 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)) { //提交我们的额任务到workQueue
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false)) //使用maximumPoolSize作为边界
        reject(command); //还不行?拒绝提交的任务
}

step 5:<ThreadPoolExecutor>
private boolean addWorker(Runnable firstTask, boolean core) 


step 6:<ThreadPoolExecutor>
w = new Worker(firstTask); //包装任务
final Thread t = w.thread; //获取线程(包含任务)
workers.add(w);   // 任务被放到works中
t.start(); //执行任务

上面的流程是高度概括的,实际情况远比这复杂得多,但是我们关心的是怎么打通整个流程,所以这样分析问题是没有太大的问题的。观察上面的流程,我们发现其实关键的地方在于Worker,如果弄明白它是如何工作的,那么我们也就大概明白了线程池是怎么工作的了。下面分析一下Worker类。

worker类图
上面的图片展示了Worker的类关系图,关键在于他实现了Runnable接口,所以问题的关键就在于run方法上。在这之前,我们来看一下Worker类里面的关键成员:

final Thread thread;

Runnable firstTask; //我们提交的任务,可能被立刻执行,也可能被放到队列里面

thread是Worker的工作线程,上面的分析我们也发现了在addWorker中会获取worker里面的thread然后start,也就是这个线程的执行,而Worker实现了Runnable接口,所以在构造thread的时候Worker将自己传递给了构造函数,thread.start执行的其实就是Worker的run方法。下面是run方法的内容:

public void run() {
runWorker(this);
}

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

我们来分析一下runWorker这个方法,这就是整个线程池的核心。首先获取到了我们刚提交的任务firstTask,然后会循环从workQueue里面获取任务来执行,获取任务的方法如下:
编译前 编译后 while (1); mov eax,1 test eax,eax je foo+23h jmp foo+18h
编译前 编译后 for (;;); jmp foo+23h   

对比之下,for (;;)指令少,不占用寄存器,而且没有判断跳转,比while (1)好。
也就是说两者在在宏观上完全一样的逻辑,但是底层完全不一样,for相对于来说更加简洁明了

相关文章
|
3月前
|
Arthas 存储 运维
记Arthas实现一次CPU排查与代码热更新
本文介绍如何使用Arthas排查线上Java应用CPU占用过高问题,结合thread、watch、jad等指令定位阻塞线程与异常代码,实现无需重启服务的热更新修复,并通过profile生成火焰图进行性能分析,提升线上问题排查效率。
|
Linux 网络安全 开发工具
|
存储 数据采集 物联网
物联网技术在物流领域的应用会遇到哪些挑战?
物联网技术在物流领域的应用会遇到哪些挑战?
835 60
|
消息中间件 存储 网络协议
2020版中间件面试题总结(RabbitMQ+Kafka+ZooKeeper)
经常碰到的29道中间件面试题总结(RabbitMQ+Kafka+ZooKeeper),含答案解析
8150 96
|
XML Java 应用服务中间件
如何断点调试Tomcat源码
如何断点调试Tomcat源码 Tomcat作为一个老牌的一个Web容器框架,用途十分的广泛。无论是为了学习其框架的整体设计还是为了碰到问题更好的解决,作为程序员我们都应该对于Tomcat有一定的了解。
1332 0
|
网络安全
Cloud Toolkit 支持 SSH 代理(跳板机)
在之前的文章中,我们分别针对开发者最主流的编程工具,介绍了《如何在 IntelliJ IDEA 中部署代码到服务器》,以及《https://yq.aliyun.com/articles/698375》。
6649 107
|
Linux
几种常用Linux系统的软件镜像源配置
几种常用Linux系统的软件镜像源配置
930 0
几种常用Linux系统的软件镜像源配置
|
传感器 人工智能 网络协议
AliOS Things 操作系统简介|学习笔记
快速学习 AliOS Things 操作系统简介。
471 0
AliOS Things 操作系统简介|学习笔记
|
开发者
iOSApp隐私设置流程(App Store Connect)
在每款 App 的产品页面上,用户能了解到该 App 可能收集的某些数据类型,以及该数据是否会用于链接或跟踪他们。
1276 0
iOSApp隐私设置流程(App Store Connect)
|
消息中间件 程序员 Go
分布式事务-SAGA与消息
分布式事务分两大类,一类是XA类型的,一类是基于消息通知的事务方案。前些日子写了[分布式事务-2PC与TCC](https://mp.weixin.qq.com/s?__biz=MzUzNzAzMTc3MA==&mid=2247484814&idx=1&sn=e3467cbc3d7ae2149e8ad5c00ede9772&scene=21#wechat_redirect),这次聊一下Saga和基于消息的的事务方案。

热门文章

最新文章