• Java线程池原理分析


    1. 简介

    Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。

    • 降低资源消耗 。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    • 提高响应速度 。当任务到达时,任务可以不需要等到线程创建就能立即执行。
    • 提高线程的可管理性 。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

    2. 线程池的继承体系

    如上图,最顶层的接口 Executor 仅声明了一个方法execute。ExecutorService 接口在其父类接口基础上,声明了包含但不限于shutdown、submit、invokeAll、invokeAny 等方法。至于 ScheduledExecutorService 接口,则是声明了一些和定时任务相关的方法,比如 schedule和scheduleAtFixedRate。线程池的核心实现是在 ThreadPoolExecutor 类中,我们使用 Executors 调用newFixedThreadPool、newSingleThreadExecutor和newCachedThreadPool等方法创建线程池均是 ThreadPoolExecutor 类型。

    3. 核心参数分析

    核心参数的配置在我的这篇博客《线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式》中有详细介绍,也可以移步参考。

    3.1 核心参数简介

    线程池的核心实现即 ThreadPoolExecutor 类。该类包含了几个核心属性,这些属性在可在构造方法进行初始化。在介绍核心属性前,我们先来看看 ThreadPoolExecutor 的构造方法,如下:

        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler)

    线程池的核心参数有如上七个,各个参数意义如下:

    3.2 线程创建规则

    在 Java 线程池实现中,线程池所能创建的线程数量受限于 corePoolSize 和 maximumPoolSize 两个参数值。线程的创建时机则和 corePoolSize 以及 workQueue 两个参数有关。

     执行图示如下:

    3.3 资源回收

    考虑到系统资源是有限的,对于线程池超出 corePoolSize 数量的空闲线程应进行回收操作。进行此操作存在一个问题,即回收时机。目前的实现方式是当线程空闲时间超过 keepAliveTime 后,进行回收。除了核心线程数之外的线程可以进行回收,核心线程内的空闲线程也可以进行回收。回收的前提是allowCoreThreadTimeOut属性被设置为 true,通过public void allowCoreThreadTimeOut(boolean) 方法可以设置属性值。

    3.4 排队策略

    当线程数量大于等于 corePoolSize,workQueue 未满时,则缓存新任务。这里要考虑使用什么类型的容器缓存新任务,通过 JDK 文档介绍,我们可知道有3中类型的容器可供使用,分别是同步队列有界队列无界队列。对于有优先级的任务,这里还可以增加优先级队列。以上所介绍的4中类型的队列,对应的实现类如下:

    3.5 拒绝策略

    当线程数量大于等于 maximumPoolSize,且 workQueue 已满,则使用拒绝策略处理新任务。Java 线程池提供了4种拒绝策略实现类,AbortPolicy 是线程池实现类所使用的策略。

    4. 线程池的操作

    4.1 线程的创建与复用

    下面是实现类中一些比较重要的成员变量:

    private final BlockingQueue<Runnable> workQueue;              //任务缓存队列,用来存放等待执行的任务
    private final ReentrantLock mainLock = new ReentrantLock();   //线程池的主要状态锁,对线程池状态(比如线程池大小、runState等)的改变都要使用这个锁
    private final HashSet<Worker> workers = new HashSet<Worker>();  //用来存放工作集
    private volatile long  keepAliveTime;    //线程存活时间   
    private volatile boolean allowCoreThreadTimeOut;   //是否允许为核心线程设置存活时间
    private volatile int   corePoolSize;     //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
    private volatile int   maximumPoolSize;   //线程池最大能容忍的线程数
    private volatile int   poolSize;       //线程池中当前的线程数
    private volatile RejectedExecutionHandler handler; //任务拒绝策略
    private volatile ThreadFactory threadFactory;   //线程工厂,用来创建线程
    private int largestPoolSize;   //用来记录线程池中曾经出现过的最大线程数
    private long completedTaskCount;   //用来记录已经执行完毕的任务个数

    在线程池的实现上,线程的创建是通过线程工厂接口ThreadFactory的实现类来完成的。默认情况下,线程池使用Executors.defaultThreadFactory()方法返回的线程工厂实现类。当然,我们也可以通过 public void setThreadFactory(ThreadFactory)方法进行动态修改。

    +----ThreadPoolExecutor.Worker.java
    Worker(Runnable firstTask) {
        setState(-1);
        this.firstTask = firstTask;
        // 调用线程工厂创建线程
        this.thread = getThreadFactory().newThread(this);
    }
    
    // Worker 实现了 Runnable 接口
    public void run() {
        runWorker(this);
    }
    
    +----ThreadPoolExecutor.java
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock();
        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);
        }
    }

    关于Worker的实现可以查看这篇博客《线程池ThreadPoolExecutor——Worker源码解析》,篇幅原因,这里不做展开。

    4.2 提交任务

    通常情况下,我们可以通过线程池的submit方法提交任务。被提交的任务可能会立即执行,也可能会被缓存或者被拒绝。任务的处理流程如下图所示:

     对应代码:

    +---- AbstractExecutorService.java
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        // 创建任务
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        // 提交任务
        execute(ftask);
        return ftask;
    }
    
    +---- ThreadPoolExecutor.java
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
    
        int c = ctl.get();
        // 如果工作线程数量 < 核心线程数,则创建新线程
        if (workerCountOf(c) < corePoolSize) {
            // 添加工作者对象
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        
        // 缓存任务,如果队列已满,则 offer 方法返回 false。否则,offer 返回 true
        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);
        }
        
        // 添加工作者对象,并在 addWorker 方法中检测线程数是否小于最大线程数
        else if (!addWorker(command, false))
            // 线程数 >= 最大线程数,使用拒绝策略处理任务
            reject(command);
    }

    上面代码可以看到,execute的源码非常依赖于addWorker,下面是addWorker方法的细节:

        private boolean addWorker(Runnable firstTask, boolean core) {
            retry:
            for (;;) {
                int c = ctl.get();
                int rs = runStateOf(c);
    
                // Check if queue empty only if necessary.
                if (rs >= SHUTDOWN &&
                    ! (rs == SHUTDOWN &&
                       firstTask == null &&
                       ! workQueue.isEmpty()))
                    return false;
    
                for (;;) {
                    //不能超过最大线程数
                    int wc = workerCountOf(c);
                    if (wc >= CAPACITY ||
                        wc >= (core ? corePoolSize : maximumPoolSize))
                        return false;
                    //线程总数+1,线程创建前检查是否合法,减少开销
                    if (compareAndIncrementWorkerCount(c))
                        break retry;
                    c = ctl.get();  // Re-read ctl
                    if (runStateOf(c) != rs)
                        //compareAndIncrementWorkerCount方法失败后重试
                        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 rs = runStateOf(ctl.get());
    
                        if (rs < SHUTDOWN ||
                            (rs == SHUTDOWN && firstTask == null)) {
                            //线程是否已启动
                            if (t.isAlive()) // precheck that t is startable
                                throw new IllegalThreadStateException();
                            workers.add(w);
                            int s = workers.size();
                            //线程池运行时的最大并发任务数
                            if (s > largestPoolSize)
                                largestPoolSize = s;
                            workerAdded = true;
                        }
                    } finally {
                        //释放锁
                        mainLock.unlock();
                    }
                    //启动线程
                    if (workerAdded) {
                        t.start();
                        workerStarted = true;
                    }
                }
            } finally {
                //增加线程失败,把开始时调用compareAndIncrementWorkerCount增加的值再减掉
                if (! workerStarted)
                    addWorkerFailed(w);
            }
            return workerStarted;
        }

    addWorker的几种调用场景如下:

    • addWorker(command, true):当线程数小于corePoolSize时,创建核心线程并且运行task。
    • addWorker(command, false):当核心线程数已满,阻塞队列已满,并且线程数小于maximumPoolSize时,创建非核心线程并且运行task。
    • addWorker(null, false):如果工作线程为0是,创建一个核心线程但是不运行task。(主要是避免工作队列中还有任务,但是工作线程为0,导致工作队列中的任务一直没有执行)

    可以结合源码,梳理该方法中线程创建的流程:

    1. 确定下worker是否可以创建。线程池如果已经stop,或者处在shutdown状态但是已经到达corePoolSize,那么就会直接返回false。
    2. 通过CAS来增加线程个数,此处会根据参数中的core来判断是“创建核心线程”和“创建非核心线程”。创建和新线程个数不能大于corePoolSize,创建非核心线程个数不能大于maximumPoolSize。
    3. 创建Worker,并且将这个worker加入的HashSet中。这里也使用了ReentrantLock来保证线程池的状态。addWorker操作结束之后,就释放这个ReentrantLock。

    4.3 关闭线程池

    在ThreadPoolExecutor中定义了关于线程状态的几个变量如下:

        // runState is stored in the high-order bits
        private static final int RUNNING    = -1 << COUNT_BITS;
        private static final int SHUTDOWN   =  0 << COUNT_BITS;
        private static final int STOP       =  1 << COUNT_BITS;
        private static final int TIDYING    =  2 << COUNT_BITS;
        private static final int TERMINATED =  3 << COUNT_BITS;
    1. 当创建线程池后,初始时,线程池处于RUNNING状态,此时线程池中的任务为0
    2. 如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕
    3. 如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务
    4. 当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。接着会执行terminated()函数
    5. 线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED,线程池被设置为TERMINATED状态

    我们可以通过shutdownshutdownNow两个方法关闭线程池。两个方法的区别在于,shutdown 会将线程池的状态设置为SHUTDOWN,同时该方法还会中断空闲线程。shutdownNow 则会将线程池状态设置为STOP,并尝试中断所有的线程。中断线程使用的是Thread.interrupt方法,未响应中断方法的任务是无法被中断的。最后,shutdownNow 方法会将未执行的任务全部返回。

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            //线程池状态设置为 SHUTDOWN
            advanceRunState(SHUTDOWN);
            //中断空闲线程
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
    
    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            //线程池状态设置为 SHUTDOWN
            advanceRunState(STOP);
            //中断所有线程
            interruptWorkers();
            //未执行任务 用于return
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

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

    5. Executors

    Executors 工具类封装了几种创建线程池的方法,不过阿里开发规范已经不推荐使用Executors来创建了,所以这里简要介绍,不做探究。

    6. 线程池大小配置

    本节来讨论一个比较重要的话题:如何合理配置线程池大小,仅供参考。

    一般需要根据任务的类型来配置线程池大小:

    • 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
    • 如果是IO密集型任务,参考值可以设置为2*NCPU

    当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。

    参考:

    https://tianxiaobo.com/2018/04/17/Java-%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90/#31-%E6%A0%B8%E5%BF%83%E5%8F%82%E6%95%B0%E5%88%86%E6%9E%90

    https://www.cnblogs.com/dolphin0520/p/3932921.html

    《Java并发编程的艺术》

    https://tianxiaobo.com/2018/04/17/Java-%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90/#31-%E6%A0%B8%E5%BF%83%E5%8F%82%E6%95%B0%E5%88%86%E6%9E%90

  • 相关阅读:
    hive、sqoop、MySQL间的数据传递
    centos7配置Hadoop集群环境
    crontab定时时间解释
    Jmeter小技巧以及问题集合
    【总结】梳理下接口功能测试
    【部署问题】解决Nginx: [error] open() "/usr/local/Nginx/logs/Nginx.pid" failed(2:No such file or directory)
    【C#公共帮助类】 ToolsHelper帮助类
    【C#公共帮助类】枚举独特类
    【无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目(4)对前面的一些问题汇总和总结
    【C#公共帮助类】分页逻辑处理类
  • 原文地址:https://www.cnblogs.com/zjfjava/p/13908426.html
Copyright © 2020-2023  润新知