线程池ThreadPoolExecutor详解

官方文档介绍

ExecutorService,它使用线程池中一个或者可能多个线程执行每个提交的任务,通常使用{@link Executors}工厂方法配置。
线程池解决了两个不同的问题:它们通常在执行大量异步任务时提供改进的性能,这是由于减少了每个任务的调用开销,并且它们提供了一种绑定和管理资源的方法,包括执行任务集合时所消耗的线程。每个ThreadPoolExecutor还维护一些基本统计信息,例如已完成任务的数量。
为了在各种上下文中有用,该类提供了许多可调参数和可扩展性钩子。 但是,程序员应该使用更方便的Executors工厂方法newCachedThreadPool(无界线程池,自动线程回收),newFixedThreadPool(固定大小的线程池)和newSingleThreadExecutor(单个后台线程),预配置最常见使用方案的设置。否则,在手动配置和调整此类时,请使用以下指南:

Core and maximum pool sizes,核心和最大池大小
ThreadPoolExecutor将根据corePoolSize和maximumPoolSize设置的边界自动调整池大小
当在方法{@link #execute(Runnable)}中提交新任务并且运行的线程少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求。 如果有多个corePoolSize但运行的maximumPoolSize线程少于maximumPoolSize,则只有在队列已满时才会创建新线程。 通过设置corePoolSize和maximumPoolSize相同,您可以创建固定大小的线程池。 通过将maximumPoolSize设置为基本无限制的值(例如{@code Integer.MAX_VALUE}),您可以允许池容纳任意数量的并发任务。 最典型的情况是,核心和最大池大小仅在构造时设置,但也可以使用{@link #setCorePoolSize}和{@link #setMaximumPoolSize}动态更改。

On-demand construction,按需构造
默认情况下,即使核心线程最初只在新任务到达时创建并启动,但可以使用方法{@link #prestartCoreThread}或{@link #prestartAllCoreThreads}动态覆盖。 如果使用非空队列构造池,则可能需要预启动线程。

Creating new threads,创建新线程
使用{@link ThreadFactory}创建新线程。 如果没有另外指定,则使用{@link Executors#defaultThreadFactory},它创建的线程都在同一个{@link ThreadGroup}中,并且具有相同的{@code NORM_PRIORITY}优先级和非守护进程状态。 通过提供不同的ThreadFactory,您可以更改线程的名称,线程组,优先级,守护程序状态等。如果{@code ThreadFactory}在通过从{@code newThread}返回null而无法创建线程时,执行程序将 继续,但可能无法执行任何任务。 线程应该拥有“modifyThread”{@code RuntimePermission}。 如果使用池的工作线程或其他线程不具有此权限,则服务可能会降级:配置更改可能不会及时生效,并且关闭池可能保持可以终止但未完成的状态。

Keep-alive times
如果线程池当前具有超过corePoolSize数量的线程,则多余的线程如果空闲时间超过keepAliveTime,则将终止(请参阅{@link #getKeepAliveTime(TimeUnit)})。 这提供了一种在不主动使用池时减少资源消耗的方法。 如果池稍后变得更活跃,则将构造新线程。 也可以使用方法{@link #setKeepAliveTime(long,TimeUnit)}动态更改此参数。 使用值{@code Long.MAX_VALUE} {@link TimeUnit#NANOSECONDS}可以有效地禁止空闲线程在关闭之前终止。 默认情况下,keep-alive策略仅在有数量超过corePoolSize大小时才适用,但方法{@link #allowCoreThreadTimeOut(boolean)}也可用于将此超时策略应用于核心线程,只要keepAliveTime值不为零即可

Queuing,排队
任何{@link BlockingQueue}都可用于转移和保留提交的任务。此队列的使用与池大小调整交互:

  • 如果运行的corePoolSize线程数量少于corePoolSize,则Executor总是更喜欢添加新线程而不是排队。
  • 如果corePoolSize数量或更多数量的线程正在运行,则Executor总是更喜欢排队请求而不是添加新线程。
  • 如果请求无法排队,则会创建一个新线程,除非这会超过maximumPoolSize,在这种情况下,该任务将被拒绝。

排队有三种常规策略:

  • 直接切换。工作队列的一个很好的默认选择是{@link SynchronousQueue},它将任务交给线程而不另外保存它们。在这里,如果没有线程立即可用于运行它,则尝试对任务进行排队将失败,因此将构造新线程。此策略在处理可能具有内部依赖性的请求集时避免了锁定。直接切换通常需要无限制的maximumPoolSizes以避免拒绝新提交的任务。这反过来承认,当命令继续以比处理它们更快的速度到达时,无限制的线程增长的可能性。
  • 无界队列,使用无界队列(例如没有预定义容量的{@link LinkedBlockingQueue)将导致新任务在所有corePoolSize线程忙时在队列中等待。因此,只会创建corePoolSize线程。 (并且maximumPoolSize的值因此没有任何影响。)当每个任务完全独立于其他任务时,这可能是适当的,因此任务不会影响彼此的执行;例如,在网页服务器中。虽然这种排队方式可以有助于平滑瞬态突发请求,但它承认,当命令继续平均到达的速度超过可处理速度时,无限制的工作队列增长的可能性。
  • 有界队列。有界队列(例如,一个{@link ArrayBlockingQueue})与有限maximumPoolSizes一起使用时有助于防止资源耗尽,但可能更难以调整和控制。 队列大小和最大池大小可以相互交换:使用大型队列和小型池最小化CPU使用率,OS资源和上下文切换开销,但可能导致人为的低吞吐量。 如果任务经常阻塞(例如,如果它们是I / O绑定的),则系统可能能够为您提供比您允许的更多线程的时间。 使用小队列通常需要更大的池大小,这会使CPU更加繁忙,但可能会遇到不可接受的调度开销,这也会降低吞吐量。

拒绝任务
当Executor关闭时,以及当Executor对最大线程和工作队列容量使用有限边界并且已经饱和时,方法{@link #execute(Runnable)}中提交的新任务将被拒绝。在任何一种情况下,{@code execute}方法都会调用其{@link RejectedExecutionHandler}的{@link RejectedExecutionHandler#rejectedExecution(Runnable,ThreadPoolExecutor)}方法。提供了四种预定义的处理程序策略:

  • 在默认的{@link ThreadPoolExecutor.AbortPolicy}中,处理程序在拒绝时抛出运行时{@link RejectedExecutionException}。
  • 在{@link ThreadPoolExecutor.CallerRunsPolicy}中,调用{@code execute}本身的线程运行该任务。 这提供了一种简单的反馈控制机制,可以降低新任务的提交速度。
  • 在{@link ThreadPoolExecutor.DiscardPolicy}中,简单地删除了无法执行的任务。
  • 在{@link ThreadPoolExecutor.DiscardOldestPolicy}中,如果执行程序未关闭,则会删除工作队列头部的任务,然后重试执行(可能会再次失败,导致重复执行)。

可以定义和使用其他类型的{@link RejectedExecutionHandler}类。 这样做需要一些小心,特别是当策略设计为仅在特定容量或排队策略下工作时

Hook methods
此类提供被调用的{@code protected} overridable {@link #beforeExecute(Thread,Runnable)}和{@link #afterExecute(Runnable,Throwable)}方法执行每项任务之前和之后。 这些可以用来操纵执行环境; 例如,重新初始化ThreadLocals,收集统计信息或添加日志条目。 此外,可以重写方法{@link #terminated}以执行Executor完全终止后需要执行的任何特殊处理。
如果hook,callback或BlockingQueue方法抛出异常,内部工作线程可能会失败,突然终止,并可能被替换。

Queue maintenance,队列维护
方法{@link #getQueue()}允许访问工作队列以进行监视和调试。 强烈建议不要将此方法用于任何其他目的。 当大量排队的任务被取消时,两个提供的方法{@link #remove(Runnable)}和{@link #purge}可用于协助存储回收。

Finalization
程序中不再引用且没有剩余线程的池将自动{@code shutdown}。 如果您希望确保即使用户忘记调用{@link #shutdown}也会回收未引用的池,那么您必须通过设置适当的保持活动时间,使用零核心线程的下限来安排未使用的线程最终死亡 或者设置{@link #allowCoreThreadTimeOut(boolean)}。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

ctl是主要的线程池控制状态,把两个变量包装成一个AtomicInteger
workerCount,指示线程池中线程的有效数量
runState,指示线程池是否运行、关闭等

workerCount代表允许启动但是不允许停止的workers的数量,这个值可能短暂不同于存活的线程的实际数量,例如:当一个ThreadFactory在被调用时无法创建一个线程,当退出线程在终止之前仍在执行bookkeeping时。用户可见的线程池大小将记录为worker集合的当前大小。
runState提供主要的生命周期控制,有以下值:
RUNNING:接受新任务并处理排队任务
SHUTDOWN:不接受新任务,但处理排队任务
STOP:不接受新任务,不处理排队任务,并中断正在进行的任务
TIDYING:所有任务都已终止,workerCount为零,转换到状态TIDYING的线程将运行terminate()钩子方法
TERMINATED:方法terminate()已完成
这些值之间的数字顺序很重要,以允许有序比较。 runState随着时间的推移单调增加,但不需要命中每个状态。过渡是:
RUNNING -> SHUTDOWN:在调用shutdown()时,可能隐含在finalize()中
(RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()
SHUTDOWN -> TIDYING:当队列和池都为空时
STOP -> TIDYING:当线程池为空
TIDYING -> TERMINATED:当terminate()钩子方法完成后

当状态达到TERMINATED时,在awaitTermination()中等待的线程将返回。
检测从SHUTDOWN到TIDYING的转换不如你想要的那么简单,因为在SHUTDOWN状态期间队列可能在非空后变为空,反之亦然,但我们只能在看到队列为空后又看到workerCount为0(有时需要重新检查)时终止。

方法介绍:
从队列中反复获取任务并执行它们,同时解决了很多问题:

  1. 我们可以从一个initial task开始,在这种情况下,我们不需要获得第一个任务。否则,只要pool正在running,我们就会从getTask获取任务。如果它返回null,则由于pool state or configuration parameters的更改而退出worker。其他退出是由外部代码中的异常抛出引起的,在这种情况下,completedAbruptly持有,这通常会导致processWorkerExit替换此线程。
  2. 在运行任何任务之前,获取锁以防止在任务执行期间其他pool interrupts,然后我们确保除非pool is stopping,否则此线程不会设置其中断。
  3. 每个任务运行之前都会调用beforeExecute,这可能会抛出异常,在这种情况下,我们会导致线程死亡(使用completedAbruptly打破循环为true)而不处理任务。
  4. 假设beforeExecute正常完成,我们运行任务,收集任何抛出的异常以发送到afterExecute。 我们分别处理RuntimeException,Error(两个规范保证我们捕获)和任意Throwables。 因为我们无法在Runnable.run中重新抛出Throwables,所以我们将它们包含在出错的Errors中(到线程的UncaughtExceptionHandler)。 任何抛出的异常也会保守地导致线程死亡。
  5. 在task.run完成之后,我们调用afterExecute,这也可能抛出异常,这也会导致线程死亡。 根据JLS Sec 14.20,该异常将生效,即使task.run抛出。

异常机制的净效果是afterExecute和线程的UncaughtExceptionHandler具有我们可以提供的关于用户代码遇到的任何问题的准确信息。

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

根据当前配置设置执行blocking或timed wait for a task,如果此worker必须因以下任何一项而退出,则返回null:

  1. 有数量超过maximumPoolSize大小的worker(由于调用setMaximumPoolSize)。
  2. 线程池被stop
  3. 线程池被shutdown 且队列为空
  4. 该worker等待一项任务超时,并且在定时等待之前和之后,超时的workers将被终止(即{@code allowCoreThreadTimeOut || workerCount> corePoolSize}),如果队列是非空,这个worker就不会是线程池中的最后一个线程。
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;
            }
        }
    }

配置线程池的正确姿势:
首先分析任务特性:

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

任务性质不同的任务可以用不同规模的线程池分开处理。
CPU密集型任务配置尽可能少的线程数量,稍微大于CPU数量即可。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。
我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次我们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞住,任务积压在线程池里。如果当时我们设置成无界队列,线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然我们的系统所有的任务是用的单独的服务器部署的,而我们使用不同规模的线程池跑不同类型的任务,但是出现这样问题时也会影响到其他任务。

线程池的监控
通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用

taskCount:线程池需要执行的任务数量。
completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
getActiveCount:获取活动的线程数。
通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值