线程池从零认识到深层理解——进阶

线程系列目录

  1. Thread线程从零认识到深层理解——初识
  2. Thread线程从零认识到深层理解——六大状态
  3. Thread线程从零认识到深层理解——wait()与notify()
  4. Thread线程从零认识到深层理解——线程安全
  5. 线程池从零认识到深层理解——初识
  6. 线程池从零认识到深层理解——进阶


注意:本系列博文源码分析取自于Android SDK=30,与网络上的一些源码可能不一样,可能他们分析的源码更旧,无需大惊小怪。

前言

《线程池从零认识到深层理解——初识》中我们已基本了解了线程的优缺点、如何创建,根据不同的初始化参数配置更适合项目使用的线程池实例,本章内容将详细讲解线程池的相关进阶内容,请往下细读。


一、线程创建策略

线程池中线程的创建时机和corePoolSize以及workQueue 两个参数有关,它有自己的一定的逻辑规律。

线程情况策略
线程数量小于 corePoolSize直接创建新线程处理新的任务
线程数量大于等于 corePoolSize,workQueue 未满则缓存新任务
线程数量大于等于 corePoolSize,但小于 maximumPoolSize,且 workQueue 已满则创建新线程处理新任务
线程数量大于等于 maximumPoolSize,且 workQueue 已满则使用拒绝策略处理新任务

线程创建的逻辑图如下:
在这里插入图片描述

二、线程资源回收策略

考虑到系统资源是有限的,对于线程池超出 corePoolSize 数量的空闲线程应进行回收操作。进行此操作存在一个问题,即回收时机。目前的实现方式是当线程空闲时间超过 keepAliveTime 后,进行回收。

除了核心线程数之外的线程可以进行回收,核心线程内的空闲线程也可以进行回收。回收的前提是allowCoreThreadTimeOut属性被设置为 true。

 public void allowCoreThreadTimeOut(boolean value) {
 	...
 }

三、任务队列阻塞策略

如上面线程创建规则所说的,当线程数量大于等于corePoolSize,workQueue未满时,则缓存新任务。这里要考虑使用什么类型的容器缓存新任务,通过 JDK 文档介绍,我们可知道有3种类型的容器可供使用,分别是同步队列,有界队列和无界队列。对于有优先级的任务,这里还可以增加优先级队列,他们都是阻塞队列。

以上所介绍的4种类型的队列,对应的实现类如下:

实现类类型说明
SynchronousQueue同步队列该队列不存储元素,每个插入操作必须等待另一个线程调用移除操作,否则插入操作会一直阻塞
ArrayBlockingQueue有界队列基于数组的阻塞队列,按照 FIFO 原则对元素进行排序
LinkedBlockingQueue无界队列基于链表的阻塞队列,按照 FIFO 原则对元素进行排序,任务队列可存储接近无限多个任务
PriorityBlockingQueue优先级队列具有优先级的阻塞队列
DelayedWorkQueue等待队列等待一段时间取用任务

ArrayBlockingQueue与LinkedBlockingQueue

LinkedList和ArrayList就是一个是用数组实现的,一个使用链表实现的,它们都是FIFO的。区别在于LinkedBlockingQueue是无界队列,就是队列元素可以无上限的队列。根据线程创建策略可知,如果队列无上限,那么就不需要非核心线程了,所以看到线程池的队列使用的是无限队列,就知道corePoolSize=maximumPoolSize。

SynchronousQueue
同步队列无法保存任务,会直接将任务交给线程。如果不存在可立即执行的任务线程,则试图将任务加入队列失败,会尝试新建一个新的线程执行任务。这种队列无缓存任务的用途,所以为了避免拒绝任务提交,一般maximumPoolSizes=Integer.MAX_VALUE

PriorityBlockingQueue
优先级队列,这种队列在向线程池中提交任务的时候会检测每一个任务的优先级,会先把优先级高的任务扔到线程池中。所以提交的任务一般需要开发者设定任务的优先级。


什么是阻塞队列?

阻塞队列是一个在队列基础上又支持了两个附加操作的队列。
支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。
支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。

线程池为什么需要阻塞队列?

线程池若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成CPU过度切换。同时线程的创建过程需要获取mainlock这个全局锁,会影响并发效率,所以需要一个阻塞队列进行缓冲。

为什么使用阻塞队列而不使用非阻塞队列?

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。使得在线程不至于一直占用cpu资源。


四、任务拒绝策略

如上线程创建规则策略中所说,当线程数量大于等于 maximumPoolSize,且 workQueue 已满,或者是当前线程池被关闭了则使用拒绝策略处理新任务。

Java 线程池提供了4种拒绝策略实现类, 如下:

实现类说明
AbortPolicy丢弃新任务,并抛出 RejectedExecutionException
DiscardPolicy不做任何操作,直接丢弃新任务
DiscardOldestPolicy丢弃队列列首的元素,并执行新任务
CallerRunsPolicy会在线程池当前正在运行的Thread线程池中处理被拒绝的任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。

以上4个拒绝策略中,AbortPolicy 是线程池实现类所使用的默认策略。我们也可以通过ThreadPoolExecutor的如下方法来修改线程池的拒绝策略。

public void setRejectedExecutionHandler(RejectedExecutionHandler handler) {
	...
}

五、线程如何创建

在线程池的实现上,线程的创建是通过线程工厂接口ThreadFactory的实现类来完成的。默认情况下会返回一个ThreadFactory的实现类DefaultThreadFactory对象。

   public static ThreadFactory defaultThreadFactory() {
        return new DefaultThreadFactory();
    }
    /**
     * The default thread factory
     */
    private static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

		/**
		* 创建线程设置优先级和线程名
		*/
        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

当然,我们也可以通过方法setThreadFactory(ThreadFactory threadFactory),我们也可以设定为自定义的线程工厂。


六、线程池如何复用

线程的复用核心代码主要如下,其核心思想:

  1. 在runWorker()方法中有一个while循环,当我们的task不为空的时候它就永远在循环,并且会源源不断的从getTask()获取新的任务。获取到task后就调用task.run()执行具体任务
  2. getTask()方法中获取的任务取自任务队列对象workQueue中
  3. 当不能再获取到任务task后,执行 processWorkerExit(w, completedAbruptly),删除当前worker以及更新workcount等操作
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        ...
        try {
            while (task != null || (task = getTask()) != null) {
                try {
                ...
                    task.run();
                } catch () {
                ...
                } finally {
                    ...
                }
            }
            ...
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

	private Runnable getTask() {
       ...//代码
       // 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;
        }
    }
}

七、任务提交方式

任务的提交有两种方式execute(Runnable command)和submit(Runnable task)。

两者的区别:

  1. 两者从方法返回值来看execute()没有返回值,submit()返回一个Future
  2. submit()方法最终也调用了execute()方法。
  3. submit在执行过程中与execute不一样,不会抛出异常而是把异常保存在RunnableFuture成员变量中,在FutureTask.get阻塞获取的时候再把异常抛出来。通过Future可以很轻易地获得任务的执行情况,比如是否执行完成、是否被取消、是否异常等等。

execute
execute方法用于提交任务,根据线程的创建策略,提交任务后进行新线程的创建或使用已有线程执行任务,如果任务无法提交,说明线程池已经关闭或者达到容量上限,此时有拒绝策略进行处理。

任务提交流程:

  1. 当正在运行线程数<corePoolSize时,如果线程池已停止或关闭,或者线程工厂无法创建创建线程则无法提交任务,进入下一流程。反之则任务提交成功与下面步骤无关。针对的是核心线程的判断。
  2. 当第一步任务提交失败后,即核心线程创建失败。当工作线程>=corePoolSizes时,如果线程池正在运行且任务能成功排队,再次检查线程池状态。如果线程池不再运行且提交的任务已回滚,则执行拒绝策略;如果线程池还处于运行状态,则新建新的非核心线程执行任务
  3. 如果任务提交排队失败,则进行拒绝策略处理。
  /**
     * Executes the given task sometime in the future.
     * 在将来的某个时间执行给定的任务
     * The task may execute in a new thread or in an existing pooled thread.
     * 该任务可以在新线程或现有池线程中执行
     * <p>
     * If the task cannot be submitted for execution, either because this
     * executor has been shutdown or because its capacity has been reached,
     * the task is handled by the current {@code RejectedExecutionHandler}.
     * 如果任务无法提交执行,是因为此执行程序已关闭或者因为其容量已达到,碰到该种情况由拒绝策略进行处理
     *
     * @param command the task to execute
     * @throws RejectedExecutionException 如果任务无法提交,由拒绝策略进行处理
     * @throws NullPointerException       if {@code command} is null
     */
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         * 进行3个步骤
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.
         * 如果正在运行的线程少于corePoolSize线程,请尝试*使用给定命令作为其第一个任务*启动新线程
         *
         * 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.
         * 对addWorker的调用从原子上检查runState和workerCount,
         * 从而通过返回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);
    }

submit
熟悉了解了execute的任务提交流程,那么submit提交流程仅仅是将Runnable包装成FutureTask,然后在执行完execute()方法后将FutureTask对象ftask返回,ftask中包含有任务的执行情况信息,比如是否执行完成、是否被取消、是否异常等等。


    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

八、按需配置线程池

如何配置更适合项目的线程池,需要根据实际情况来判断创建,一般分为如下几种:

  1. CPU密集型任务:
    尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务需要CPU使用率很高,若开过多的线程数,会造成CPU过度切换,CPU的实际计算使用时间缩短。

  2. IO密集型任务:
    可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,但是需要进行大量的耗时操作。因此可以让CPU在等待IO的时候让其他线程去处理别的任务,充分利用CPU时间。

  3. 混合型任务:
    可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。


九、关闭线程池

在程序退出程序时,我们可以考虑将线程池关闭,根据关闭方法将一些待执行或执行中的任务关闭掉,程序都推出了也就没必要继续执行任务了。

线程池提供了shutdown和shutdownNow两个方法关闭线程池。调用 shutdown 和 shutdownNow 方法关闭线程池后,就不能再向线程池提交新任务了,会使用拒绝策略处理新提交的任务。

shutdown
有序的关闭线程,该方法调用后原先已提交的任务还是会执行,但是不会再接受新任务,并进行拒绝策略处理。shutdown任务的重复提交不会有任何其他影响

如果调用shutdown()方法时,已有线程在执行任务或任务队列中还有任务会继续执行,该方法不会等待先前提交任务的完成。该方法会将线程池的状态设置为SHUTDOWN,同时会中断空闲线程。

   /**
     * 启动有序关闭,在该关闭中执行先前提交的任务,但不接受任何新任务。
     * 如果调用已经关闭,则调用不会产生任何其他影响。
     * 该方法会将线程池标记为SHUTDOWN,且不会等待已经提交的任务完成,会使用#awaitTermination方法处于等待状态,
     * 一旦超过线程池关闭等待时机,将会将线程池标记为TERMINATED状态
     */
    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            //检查调用者具有关闭线程的权限,如果安全管理器通过了该权限,确保允许调用者中断每个工作线程
            checkShutdownAccess();
            //标定其状态为SHUTDOWN
            advanceRunState(SHUTDOWN);
            //中断空闲线程
            interruptIdleWorkers();
            // 专用于ScheduledThreadPoolExecutor中取消定时任务
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        //关闭信号传播,拒绝接受新任务,如果处于SHUTDOWN时线程池为空且队列为空将状态转变为TERMINATED,
        //或者处于STOP状态时线程池为空,则将线程池转为TERMINATED状态
        tryTerminate();
    }

根据源码分析,其逻辑流程是:

  1. 检查调用者是否有shutdown权限,如果调用者有权限,确保允许中断每个工作线程
  2. 将线程的状态设置为SHUTDOWN
  3. 中断空闲线程,线程池是逐步关闭的,当线程空闲下来就会被中断
  4. onShutdown()仅用于ScheduledThreadPoolExecutor中取消定时任务
  5. tryTerminate()尝试将线程池的状态设置为TERMINATED,当任务队列中无任务且线程池中无工作线程时,可以成功将其状态改变为TERMINATED,否则只能等待#awaitTermination方法中来进行状态的处理。

shutdownNow
shutdownNow会将线程池状态设置为STOP,并尝试中断所有的线程(包括正在执行任务的线程)。中断线程使用的是Thread.interrupt方法,未响应中断方法的任务是无法被中断的。最后,shutdownNow 方法会将未执行的任务全部返回。

    /**
     * 尝试停止所有正在执行的任务,中止正在等待的任务的处理,
     * 并返回正在等待执行的任务的列表,原始任务队列删除所有任务元素
     *
     * <p>This method does not wait for actively executing tasks to terminate.
     * 对于已经执行的任务,但又无法打断的任务,不会等待。其状态改变交给awaitTermination()
     *
     * 除了尽最大的努力,无法保证一定能停止处理正在执行的任务。因为中断通过{@link Thread#interrupt}中断任务,无法响应中断的任务可能永远不会终止。
     */
    // android-note: Removed @throws SecurityException
    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            //检查调用者具有关闭线程的权限,如果安全管理器通过了该权限,确保允许调用者中断每个工作线程
            checkShutdownAccess();
            //标定其状态为STOP
            advanceRunState(STOP);
            //中断所有线程,不一定能中断成功
            interruptWorkers();
            //返回待执行任务列表
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        // 尝试完全关闭线程池
        tryTerminate();
        return tasks;
    }

根据源码分析,其逻辑流程是:

  1. 检查调用者是否有shutdown权限,如果调用者有权限,确保允许中断每个工作线程
  2. 将线程的状态设置为SHUTDOWN
  3. interruptWorkers()中断所有的线程,即使是活跃状态。会出现部分线程中断异常,无法被中断的情况
  4. 获取到待执行任务的任务列表,最终返回给调用者
  5. tryTerminate()尝试将线程池的状态设置为TERMINATED,当线程池中无可用工作线程时,可以成功将状态转变为TERMINATED。否则只能等待#awaitTermination方法中来进行状态的处理

awaitTermination
当调用shutdown或者shutdownNow后,如果tryTerminate()尝试将线程池状态改变为TERMINATED失败后。线程池的最终终止工作有awaitTermination()方法处理

    public boolean awaitTermination(long timeout, TimeUnit unit)
            throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            while (!runStateAtLeast(ctl.get(), TERMINATED)) {
                if (nanos <= 0L)
                    return false;
                nanos = termination.awaitNanos(nanos);
            }
            return true;
        } finally {
            mainLock.unlock();
        }
    }

方法中提供设定线程池强制关闭超时时间,当参数nanos=0时,使用默认策略。当nanos>0时,线程调用关闭方法后,如果超过超时时间,线程池将会强制关闭。 默认策略是所有工作线程执行完所有任务后关闭线程池。


总结

《线程池从零认识到深层理解——初识》博文中已经对线程池进行了初步的认知,和常见的线程池的区别和创建。

本篇博文主要是对线程池的进阶学习,知道了各种策略和原理才能将线程池用的更有高效和符合项目,对于自己知识体系也是一种不足和巩固。


博客书写不易,您的点赞收藏是我前进的动力,千万别忘记点赞、 收藏 ^ _ ^ !

相关链接

  1. Thread线程从零认识到深层理解——初识
  2. Thread线程从零认识到深层理解——六大状态
  3. Thread线程从零认识到深层理解——wait()与notify()
  4. Thread线程从零认识到深层理解——线程安全
  5. 线程池从零认识到深层理解——初识
  6. 线程池从零认识到深层理解——进阶
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值