JDK线程池原理之一:工作原理

一、线程池工作原理(新任务进入时线程池的执行策略)
二、创建线程池的方式
三、合理配置线程池
四、ThreadPoolExecutor的函数说明

 

一、线程池工作原理(新任务进入时线程池的执行策略)

线程池的触发时机如下图:

当一个任务通过execute(Runnable)方法欲添加到线程池时: 

1、如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

2、如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列workQueue中,等待线程池中线程调度执行。

3、如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

4、当线程池中的线程执行完任务空闲时,会尝试从workQueue中取头结点任务执行。

5、如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

6、当线程池中线程数超过corePoolSize,并且配置allowCoreThreadTimeOut=true,空闲时间超过keepAliveTime的线程会被销毁,保持线程池中线程数为corePoolSize。(注:销毁空闲线程,保持线程数为corePoolSize,不是销毁corePoolSize中的线程。)

 

再看看源码java.util.concurrent.ThreadPoolExecutor.java

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        //判断当前活跃线程数是否小于corePoolSize
        if (workerCountOf(c) < corePoolSize) {
            //如果小于,则调用addWorker创建线程执行任务
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //如果大于等于corePoolSize,则将任务添加到workQueue队列
        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);
        }
       //如果放入workQueue队列失败,则创建非核心线程执行任务
        else if (!addWorker(command, false))
            reject(command);
    }

 附一个线程池底层示意图:

 

 

二、创建线程池的方式

创建线程池对象,强烈建议通过使用ThreadPoolExecutor的构造方法创建,不要使用Executors阿里《Java开发手册》中的一段描述。

【强制】线程池不允许使用Executors创建,建议通过ThreadPoolExecutor的方式创建,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors返回的线程池对象的弊端如下:

1.FixedThreadPool和SingleThreadPool: 允许的请求队列长度为Integet.MAX_VALUE,可能会堆积大量的请求从而导致OOM;

2.CachedThreadPool: 允许创建线程数量为Integet.MAX_VALUE,可能会创建大量的线程,从而导致OOM.

使用自定义线程工厂:当项目规模逐渐扩展,各系统中线程池也不断增多,当发生线程执行问题时,通过自定义线程工厂创建的线程设置有意义的线程名称可快速追踪异常原因,高效、快速的定位问题。

使用自定义拒绝策略:虽然,JDK给我们提供了一些默认的拒绝策略,但我们可以根据项目需求的需要,或者是用户体验的需要,定制拒绝策略,完成特殊需求。

线程池划分隔离:不同业务、执行效率不同的分不同线程池,避免因某些异常导致整个线程池利用率下降或直接不可用,进而影响整个系统或其它系统的正常运行。

三、合理配置线程池

分两种情况:

CPU密集

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。

CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。

IO密集

IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析:

  1. 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。
  2. 任务的优先级:高、中、低。
  3. 任务的执行时间:长、中、短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接等。

性质不同的任务可以交给不同规模的线程池执行:

  1. CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数;
  2. IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1;

对于混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。

若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。

当然具体合理线程池值大小,需要结合系统实际情况,在大量的尝试下比较才能得出,以上只是前人总结的规律。

 四、ThreadPoolExecutor的函数说明

主要成员函数
public void execute(Runnable command)
    在将来某个时间执行给定任务。可以在新线程中或者在现有池线程中执行该任务。 如果无法将任务提交执行,或者因为此执行程序已关闭,或者因为已达到其容量,则该任务由当前 RejectedExecutionHandler 处理。
    参数:
        command - 要执行的任务。 
    抛出:
        RejectedExecutionException - 如果无法接收要执行的任务,则由 RejectedExecutionHandler 决定是否抛出 RejectedExecutionException 
        NullPointerException - 如果命令为 null
public void shutdown()
    按过去执行已提交任务的顺序发起一个有序的关闭,但是不接受新任务。如果已经关闭,则调用没有其他作用。
    抛出:
        SecurityException - 如果安全管理器存在并且关闭此 ExecutorService 可能操作某些不允许调用者修改的线程(因为它没有 RuntimePermission("modifyThread")),或者安全管理器的 checkAccess 方法拒绝访问。
public List<Runnable> shutdownNow()
    尝试停止所有的活动执行任务、暂停等待任务的处理,并返回等待执行的任务列表。在从此方法返回的任务队列中排空(移除)这些任务。
    并不保证能够停止正在处理的活动执行任务,但是会尽力尝试。 此实现通过 Thread.interrupt() 取消任务,所以无法响应中断的任何任务可能永远无法终止。
    返回:
        从未开始执行的任务的列表。 
    抛出:
        SecurityException - 如果安全管理器存在并且关闭此 ExecutorService 
        可能操作某些不允许调用者修改的线程(因为它没有 RuntimePermission("modifyThread")),
        或者安全管理器的 checkAccess 方法拒绝访问。
public int prestartAllCoreThreads()
    启动所有核心线程,使其处于等待工作的空闲状态。仅当执行新任务时,此操作才重写默认的启动核心线程策略。
    返回:
        已启动的线程数
public boolean allowsCoreThreadTimeOut()
    如果此池允许核心线程超时和终止,如果在 keepAlive 时间内没有任务到达,新任务到达时正在替换(如果需要),则返回 true。当返回 true 时,适用于非核心线程的相同的保持活动策略也同样适用于核心线程。当返回 false(默认值)时,由于没有传入任务,核心线程不会终止。
    返回:
        如果允许核心线程超时,则返回 true;否则返回 false
public void allowCoreThreadTimeOut(boolean value)
    如果在保持活动时间内没有任务到达,新任务到达时正在替换(如果需要),则设置控制核心线程是超时还是终止的策略。当为 false(默认值)时,由于没有传入任务,核心线程将永远不会中止。当为 true 时,适用于非核心线程的相同的保持活动策略也同样适用于核心线程。为了避免连续线程替换,保持活动时间在设置为 true 时必须大于 0。通常应该在主动使用该池前调用此方法。
    参数:
        value - 如果应该超时,则为 true;否则为 false 
    抛出:
        IllegalArgumentException - 如果 value 为 true 并且当前保持活动时间不大于 0。
public boolean remove(Runnable task)
    从执行程序的内部队列中移除此任务(如果存在),从而如果尚未开始,则让其不再运行。
    此方法可用作取消方案的一部分。它可能无法移除在放置到内部队列之前已经转换为其他形式的任务。
    例如,使用 submit 输入的任务可能被转换为维护 Future 状态的形式。但是,在此情况下,purge() 方法可用于移除那些已被取消的 Future。
    参数:
        task - 要移除的任务 
    返回:
        如果已经移除任务,则返回 true
public void purge()
    尝试从工作队列移除所有已取消的 Future 任务。此方法可用作存储回收操作,它对功能没有任何影响。
    取消的任务不会再次执行,但是它们可能在工作队列中累积,直到worker线程主动将其移除。
    调用此方法将试图立即移除它们。但是,如果出现其他线程的干预,那么此方法移除任务将失败。
当然它还实现了的ExecutorService的submit系列接口

abstract <T> Future<T> submit(Runnable task, T result) --如果执行成功就返回T result
abstract <T> Future<T> submit(Callable<T> task)--Submits a value-returning task for execution and returns a Future representing the pending results of the task.
abstract Future<?> submit(Runnable task) --Submits a Runnable task for execution and returns a Future representing that task.

 

钩子 (hook) 方法
    此类提供 protected是可重写的:

protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
protected void terminated() { }

 beforeExecute(java.lang.Thread, java.lang.Runnable)和 afterExecute(java.lang.Runnable, java.lang.Throwable) 方法,这两种方法分别在执行每个任务之前和之后调用。

 它们可用于操纵执行环境;例如,重新初始化 ThreadLocal、搜集统计信息或添加日志条目等。
 此外,还可以重写方法 terminated() 来执行 Executor 完全终止后需要完成的所有特殊处理。
 如果钩子 (hook) 或回调方法抛出异常,则ThreadPoolExecutor的所有线程将依次失败并突然终止。 


终止
    如果ThreadPoolExecutor在程序中没有任何引用且没有任何活动线程,它也不会自动 shutdown。
    如果希望确保回收线程(即使用户忘记调用 shutdown()),则必须安排未使用的线程最终终止:设置适当保持活动时间,使用0核心线程的下边界和/或设置 allowCoreThreadTimeOut(boolean)。
 

 

参考:https://blog.csdn.net/qq_35448985/article/details/108096021

posted on 2021-04-01 15:29  duanxz  阅读(331)  评论(0编辑  收藏  举报