多线程(7)线程池(参数解析以及源码解析)

一、为什么使用线程池?

构建一个新的线程是有一定代价的,因为涉及与操作系统的交互。如果程序中创建了大量的生命期很短的线程,应该使用线程池。一个线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。

另一个使用线程池的理由是减少并发线程的数目。创建大量线程会大大降低性能甚至使虚拟机崩溃。如果有一个会创建许多线程的算法,应该使用一个线程数“固定的”线程池以限制并发线程的总数。

线程池的好处

1、降低资源消耗。重用线程池中的线程,避免因频繁创建和销毁线程造成的性能消耗。
2、提高响应速度。当任务到达时,任务可以不需要等到线程创建就能执行。
3、对线程进行有效的管理。使用线程池可以对线程进行统一的分配,调优和监控。

二、线程池的创建

线程池的创建需要通过ThreadPoolExecutor,可以通过ThreadPoolExecutor的构造函数创建,也可以利用执行器创建。
线程池的创建:参考文章的执行器部分

JDK中线程池的核心实现类是ThreadPoolExecutor,线程池的创建需要通过ThreadPoolExecutor。
newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor这 3 个静态工厂方法返回实现了ExecutorService 接口的 ThreadPoolExecutor 类的对象。例如:

public static ExecutorService newFixedThreadPool(int nThreads) {
 	return new ThreadPoolExecutor(nThreads, nThreads,
 	0L, TimeUnit.MILLISECONDS,
 	new LinkedBlockingQueue<Runnable>());
}

三、线程池的参数

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

可以看到,线程池的构造函数里有7个参数,下面分别介绍这些参数的含义。

corePoolSize

核心线程数。

在创建线程池之后,默认情况下线程池中并没有任何的线程,而是等待任务到来才创建线程去执行任务,当线程池中的线程数目达到corePoolSize后,新来的任务将会被添加到缓存队列中,也就是那个workQueue。

maximumPoolSize

最大线程数。

表示线程池中最多可以创建多少个线程,很多人以为它的作用是这样的:”当线程池中的任务数超过 corePoolSize 后,线程池会继续创建线程,直到线程池中的线程数小于maximumPoolSize“,其实这种理解是完全错误的。它真正的作用是:当线程池中的线程数等于 corePoolSize 并且 workQueue 已满,这时就要看当前线程数是否大于 maximumPoolSize,如果小于maximumPoolSize 定义的值,则会继续创建线程去执行任务, 否则将会调用去相应的任务拒绝策略来拒绝这个任务。另外超过 corePoolSize的线程被称做"Idle Thread", 这部分线程会有一个最大空闲存活时间(keepAliveTime),如果超过这个空闲存活时间还没有任务被分配,则会将这部分线程进行回收。

workQueue

工作队列。

阻塞队列,用来存储等待执行的任务,决定了线程池的排队策略。当新任务来的时候,先判断当前运行的线程数是否达到了corePoolSize,如果达到了,新任务就会被放入队列中。

Java中常用的阻塞队列:阻塞队列

keepAliveTime

存活时间。

一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定。

unit

keepAliveTime的计量单位。

是一个枚举类型。 有NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS,7个可选值。

threadFactory

线程工厂。

创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。当我们不指定线程工厂时,线程池内部会调用Executors.defaultThreadFactory()创建默认的线程工厂,其后续创建的线程优先级都是Thread.NORM_PRIORITY。如果我们指定线程工厂,我们可以对产生的线程进行一定的操作。

handler

拒绝策略。

当线程数量已经达到最大线程数量,并且队列已满的情况下,如果有新任务来就会执行拒绝策略。

拒绝策略有以下几种:

  1. AbortPolicy(默认策略)

该策略下,直接丢弃任务,并抛出RejectedExecutionException异常

  1. CallerRunsPolicy

该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。(会降低对于新任务的提交速度,影响程序的整体性能)

  1. DiscardPolicy

该策略下,直接丢弃任务,什么都不做。

  1. DiscardOldestPolicy

该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

线程池工作流程

在这里插入图片描述

四、线程池原理(源码解析)

ThreadPoolExecutor 是线程池的核心,提供了线程池的实现。前面说过,newFixedThreadPool等方法最后会返回一个ThreadPoolExecutor的对象,以newFixedThreadPool为例,可以先看下它的流程图:
在这里插入图片描述
具体的代码会在下面分析。

ThreadPoolExecutor

首先看下ThreadPoolExecutor的部分源码:

这个类的第一个成员变量ctl,AtomicInteger这个类可以通过CAS达到无锁并发,效率比较高,这个变量有双重身份,它的高3位表示线程池的状态(状态有5种,所以需要3位),低29位表示线程池中现有的线程数,这也是Doug Lea一个天才的设计,用最少的变量来减少锁竞争,提高并发效率。

    //CAS,无锁并发
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //表示线程池线程数的bit数
    private static final int COUNT_BITS = Integer.SIZE - 3;
    //最大的线程数量,数量是完全够用了
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    //1110 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int RUNNING    = -1 << COUNT_BITS;
    //0000 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    //0010 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int STOP       =  1 << COUNT_BITS;
    //0100 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int TIDYING    =  2 << COUNT_BITS;
    //0110 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int TERMINATED =  3 << COUNT_BITS;

    // Packing and unpacking ctl
    //获取线程池的状态
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    //获取线程的数量
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    //组装状态和数量,成为ctl
    private static int ctlOf(int rs, int wc) { return rs | wc; }

    /*
     * Bit field accessors that don't require unpacking ctl.
     * These depend on the bit layout and on workerCount being never negative.
     * 判断状态c是否比s小,下面会给出状态流转图
     */
    
    private static boolean runStateLessThan(int c, int s) {
        return c < s;
    }
    
    //判断状态c是否不小于状态s
    private static boolean runStateAtLeast(int c, int s) {
        return c >= s;
    }
    //判断线程是否在运行
    private static boolean isRunning(int c) {
        return c < SHUTDOWN;
    }

线程池的五种状态

  1. RUNNING - 接受新任务并且继续处理阻塞队列中的任务
  2. SHUTDOWN - 不接受新任务但是会继续处理阻塞队列中的任务
  3. STOP - 不接受新任务,不在执行阻塞队列中的任务,中断正在执行的任务
  4. TIDYING - 所有任务都已经完成,线程数都被回收,线程会转到TIDYING状态会继续执行钩子方法
  5. TERMINATED - 钩子方法执行完毕

状态转化如下:
在这里插入图片描述

execute/submit

向线程池提交任务有这2种方式,execute是ExecutorService接口定义的,submit有三种方法重载都在AbstractExecutorService中定义,都是将要执行的任务包装为FutureTask来提交,使用者可以通过FutureTask来拿到任务的执行状态和执行最终的结果,最终调用的都是execute方法,其实对于线程池来说,它并不关心你是哪种方式提交的,因为任务的状态是由FutureTask自己维护的,对线程池透明。

看下execute的实现:

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

三步操作为:

  1. 检查当前线程池中的线程数是否<核心线程数,如果小于核心线程数,就调用addWorker方法创建一个新的线程执行任务,addworker中的第二个参数传入true,表示当前创建的是核心线程。如果当前线程数>=核心线程数或者创建线程失败的话,直接进入第二种情况。

  2. 通过调用isRunning方法判断线程池是否还在运行,如果线程池状态不是running,那就直接退出execute方法,没有执行的必要了;如果线程池的状态是running,尝试着把任务加入到queue中,再次检查线程池的状态, 如果当前不是running,可能在入队后调用了shutdown方法,所以要在queue中移除该任务,默认采用拒绝策略直接抛出异常。如果当前线程数为0,可能把allowCoreThreadTimeOut设为了true,正好核心线程全部被回收,所以必须要创建一个空的线程,让它自己去queue中去取任务执行。

  3. 如果当前线程数>核心线程数,并且入队失败,调用addWorker方法创建一个新的线程去执行任务,第二个参数是false,表示当前创建的线程不是核心线程。这种情况表示核心线程已满并且queue已满,如果当前线程数小于最大线程数,创建线程执行任务。如果当前线程数>=最大线程数,默认直接采取拒绝策略。

addWorker

addWorker,顾名思义,其实就是要创建一个工作线程。源码比较长,其实就做了两件事:

  • 采用循环 CAS 操作来将线程数加 1;
  • 新建一个线程并启用。
    private boolean addWorker(Runnable firstTask, boolean core) {
        retry: //goto 语句,避免死循环
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
            // Check if queue empty only if necessary.
            /*如果线程处于非运行状态,并且 rs 不等于 SHUTDOWN 且 firstTask 不等于空且且
            workQueue 为空,直接返回 false(表示不可添加 work 状态)
            1. 线程池已经 shutdown 后,还要添加新的任务,拒绝
            2. (第二个判断)SHUTDOWN 状态不接受新任务,但仍然会执行已经加入任务队列的任
            务,所以当进入 SHUTDOWN 状态,而传进来的任务为空,并且任务队列不为空的时候,是允许添加
            新线程的,如果把这个条件取反,就表示不允许添加 worker*/
            if (rs >= SHUTDOWN &&
                    ! (rs == SHUTDOWN &&
                            firstTask == null &&
                            ! workQueue.isEmpty()))
                return false;
            for (;;) { //自旋
                int wc = workerCountOf(c);//获得 Worker 工作线程数
                //如果工作线程数大于默认容量大小或者大于核心线程数大小,则直接返回 false 表示不
                能再添加 worker。
                if (wc >= CAPACITY ||
                        wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))//通过 cas 来增加工作线程数,如果 cas 失败,则直接重试
                    break retry;
                c = ctl.get(); // Re-read ctl //再次获取 ctl 的值
                if (runStateOf(c) != rs) //这里如果不想等,说明线程的状态发生了变化,继续重试
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        
        //上面这段代码主要是对 worker 数量做原子+1 操作,下面的逻辑才是正式构建一个 worker


        boolean workerStarted = false; //工作线程是否启动的标识
        boolean workerAdded = false; //工作线程是否已经添加成功的标识
        Worker w = null;
        try {
            w = new Worker(firstTask); //构建一个 Worker,这个 worker 是什么呢?我们可以看到构造方法里面传入了一个 Runnable 对象
            final Thread t = w.thread; //从 worker 对象中取出线程
            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());
                    //只有当前线程池是正在运行状态,[或是 SHUTDOWN 且 firstTask 为空],才能添加到 workers 集合中
                    if (rs < SHUTDOWN ||
                            (rs == SHUTDOWN && firstTask == null)) {
                        //任务刚封装到 work 里面,还没 start,你封装的线程就是 alive,几个意思?肯定是要抛异常出去的
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w); //将新创建的 Worker 添加到 workers 集合中
                        int s = workers.size();
                        //如果集合中的工作线程数大于最大线程数,这个最大线程数表示线程池曾经出现过的最大线程数
                        if (s > largestPoolSize)
                            largestPoolSize = s; //更新线程池出现过的最大线程数
                        workerAdded = true;//表示工作线程创建成功了
                    }
                } finally {
                    mainLock.unlock(); //释放锁
                }
                if (workerAdded) {//如果 worker 添加成功
                    t.start();//启动线程
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w); //如果添加失败,就需要做一件事,就是递减实际工作线程数(还记得我们最开始的时候增加了工作线程数吗)
        }
        return workerStarted;//返回结果
    }

把worker对象放入hashset中,hashset的底层就是hashmap实现的,hashmap是线程不安全的,所以必须要加锁。workers就是当前的工作线程。
private final HashSet<Worker> workers = new HashSet<Worker>();

从代码种可以看到,execute方法虽然没有加锁,但是在addWorker方法内部加锁了,这样可以保证不会创建超过我们预期的线程数,大师在设计的时候,做到了在最小的范围内加锁,尽量减少锁竞争。
core参数,只是用来判断当前线程数是否超量的时候跟corePoolSize还是maxPoolSize比较,Worker本身无核心或者非核心的概念。

Worker

addWorker 方法只是构造了一个 Worker,并且把 firstTask 封装到 worker 中,下面看下worker的源码:

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
        private static final long serialVersionUID = 6138294804551838833L;
        /**
         * Thread this worker is running in. Null if factory fails.
         */
        final Thread thread; //注意了,这才是真正执行 task 的线程,从构造函数可知是由ThreadFactury 创建的
        /**
         * Initial task to run. Possibly null.
         */
        Runnable firstTask; //这就是需要执行的 task
        /**
         * Per-thread task counter
         */
        volatile long completedTasks; //完成的任务数,用于线程池统计

        Worker(Runnable firstTask) {
            setState(-1); //初始状态 -1,防止在调用 runWorker(),也就是真正执行 task前中断 thread。
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

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

        ...
    }
  1. 每个 worker,都是一条线程,同时里面包含了一个 firstTask,即初始化时要被首先执行的任务。
  2. 最终执行任务的,是 runWorker()方法。

Worker 类继承了 AQS,并实现了 Runnable 接口,注意其中的 firstTask 和 thread 属性:firstTask 用它来保存传入的任务;thread 是在调用构造方法时通过 ThreadFactory 来创建的线程,是用来处理任务的线程。

在调用构造方法时,需要传入任务,这里通过 getThreadFactory().newThread(this);来新建一个线程,newThread 方法传入的参数是 this,因为 Worker 本身继承了 Runnable 接口,也就是一个线程,所以一个 Worker 对象在启动的时候会调用 Worker 类中的 run 方法。

Worker 继承了 AQS,使用 AQS 来实现独占锁的功能。为什么不使用 ReentrantLock 来实现呢?可以看到 tryAcquire 方法,它是不允许重入的,而 ReentrantLock 是允许重入的

为什么要使用独占锁:lock 方法一旦获取了独占锁,表示当前线程正在执行任务中。那么它会有以下几个作用:

  1. 如果正在执行任务,则不应该中断线程;
  2. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断;
  3. 线程池在执行 shutdown 方法或 tryTerminate 方法时会调用 interruptIdleWorkers 方法来中断空闲的线程,interruptIdleWorkers 方法会使用 tryLock 方法来判断线程池中的线程是否是空闲状态
  4. 之所以设置为不可重入,是因为我们不希望任务在调用像 setCorePoolSize 这样的线程池控制方法时重新获取锁,这样会中断正在运行的线程

addWorkerFailed

addWorker 方法中,如果添加 Worker 并且启动线程失败,则会做失败后的处理:

    private void addWorkerFailed(Worker w) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (w != null)
                workers.remove(w);
            decrementWorkerCount();
            tryTerminate();
        } finally {
            mainLock.unlock();
        }
    }

主要做三件事:

  1. 如果 worker 已经构造好了,则从 workers 集合中移除这个 worker
  2. 原子递减核心线程数(因为在 addWorker 方法中先做了原子增加)
  3. 尝试结束线程池

runWorker

前面已经了解了 ThreadPoolExecutor 的核心方法 addWorker,主要作用是增加工作线程,而 Worker 简单理解其实就是一个线程,里面重新了 run 方法,这块是线程池中执行任务的真正处理逻辑,也就是 runWorker 方法,这个方法主要做几件事:

  1. 如果 task 不为空,则开始执行 task
  2. 如果 task 为空,则通过 getTask()再去取任务,并赋值给 task,如果取到的 Runnable 不为空,则执行该任务
  3. 执行完毕后,通过 while 循环继续 getTask()取任务
  4. 如果 getTask()取到的任务依然是空,那么整个 runWorker()方法执行完毕
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        //unlock,表示当前 worker 线程允许中断,因为 new Worker 默认的 state=-1,此处是调用
        //Worker 类的 tryRelease()方法,将 state 置为 0,
        //而 interruptIfStarted()中只有 state>=0 才允许调用中断
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            //注意这个 while 循环,在这里实现了 [线程复用] // 如果 task 为空,则通过getTask 来获取任务
            while (task != null || (task = getTask()) != null) {
                w.lock(); //上锁,不是为了防止并发执行任务,为了在 shutdown()时不终止正在运行的 worker
                //线程池为 stop 状态时不接受新任务,不执行已经加入任务队列的任务,还中断正在执行的任务
                //所以对于 stop 状态以上是要中断线程的
                //(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP)确保线程中断标志位为 true 且是 stop 状态以上,接着清除了中断标志
                //!wt.isInterrupted()则再一次检查保证线程需要设置中断标志位
                if ((runStateAtLeast(ctl.get(), STOP) ||
                        (Thread.interrupted() &&
                                runStateAtLeast(ctl.get(), STOP))) &&
                        !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);//这里默认是没有实现的,在一些特定的场景中我们可以自己继承 ThreadpoolExecutor 自己重写
                    Throwable thrown = null;
                    try {
                        task.run(); //执行任务中的 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,需要再通过 getTask()取) + 记录该 Worker 完成任务数量 + 解锁
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
            //1.将入参 worker 从数组 workers 里删除掉;
            //2.根据布尔值 allowCoreThreadTimeOut 来决定是否补充新的 Worker 进数组workers
        }
    }

首先还是获取当前线程,获取当前worker对象中的任务task,把当前线程的状态由-1设为0,表示可以获取锁执行任务,接下来就是一个while循环(这个循环里实现了线程复用),当task不为空或者从gettask方法取出的任务不为空的时候,加锁,底层还是使用了AQS,保证了只有一个线程执行完毕其他线程才能执行。在执行任务之前,必须进行判断,线程池的状态如果>=STOP,必须中断当前线程,如果是running或者shutdown,当前线程不能被中断,防止线程池调用了shutdownnow方法必须中断所有的线程。

在处理任务之前,会执行beforeExecute方法, 在处理任务之后,执行afterExecute方法,这两个都是钩子方法,继承了ThreadPoolExecutor可以重写此方法,嵌入自定义的逻辑。一旦在任务运行的过程中,出现异常会直接抛出,所以在实际的业务中,应该使用try…catch,把这些日常加入到日志中。

任务执行完,就把task设为空,累加当前线程完成的任务数,unlock,继续从queue中取任务执行。

getTask

worker 线程会从阻塞队列中获取需要执行的任务,这个方法不是简单的 take 数据,我们来分析下他的源码实现:

你也许好奇是怎样判断线程有多久没有活动了,是不是以为线程池会启动一个监控线程,专门监控哪个线程正在偷懒?想太多,其实只是在线程从工作队列 poll 任务时,加上了超时限制,如果线程在 keepAliveTime 的时间内 poll 不到任务,那我就认为这条线程没事做,可以干掉了,看看这个代码片段你就清楚了

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?
        for (;;) {//自旋
            int c = ctl.get();
            int rs = runStateOf(c);
            /* 对线程池状态的判断,两种情况会 workerCount-1,并且返回 null
            1. 线程池状态为 shutdown,且 workQueue 为空(反映了 shutdown 状态的线程池还是要执行 workQueue 中剩余的任务的)
            2. 线程池状态为 stop(shutdownNow()会导致变成 STOP)(此时不用考虑 workQueue的情况)*/
            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;//返回 null,则当前 worker 线程会退出
            }
            int wc = workerCountOf(c);
            // timed 变量用于判断是否需要进行超时控制。
            // allowCoreThreadTimeOut 默认是 false,也就是核心线程不允许进行超时;
            // wc > corePoolSize,表示当前线程池中的线程数量大于核心线程数量;
            // 对于超过核心线程数量的这些线程,需要进行超时控制
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            /*1. 线程数量超过 maximumPoolSize 可能是线程池在运行时被调用了 setMaximumPoolSize()
            被改变了大小,否则已经 addWorker()成功不会超过 maximumPoolSize
            2. timed && timedOut 如果为 true,表示当前操作需要进行超时控制,并且上次从阻塞队列中
            获取任务发生了超时.其实就是体现了空闲线程的存活时间*/
            if ((wc > maximumPoolSize || (timed && timedOut))
                    && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }
            try {
                /*根据 timed 来判断,如果为 true,则通过阻塞队列 poll 方法进行超时控制,如果在
                keepaliveTime 时间内没有获取到任务,则返回 null.
                否则通过 take 方法阻塞式获取队列中的任务*/
                Runnable r = timed ?
                        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                        workQueue.take();
                if (r != null)//如果拿到的任务不为空,则直接返回给 worker 进行处理
                    return r;
                timedOut = true;//如果 r==null,说明已经超时了,设置 timedOut=true,在下次自旋的时候进行回收
            } catch (InterruptedException retry) {
                timedOut = false;// 如果获取任务时当前线程发生了中断,则设置 timedOut 为false 并返回循环重试
            }
        }
    }

这里重要的地方是第二个 if 判断,目的是控制线程池的有效线程数量。由上文中的分析可以知道,在执行 execute 方法时,如果当前线程池的线程数量超过了 corePoolSize 且小于maximumPoolSize,并且 workQueue 已满时,则可以增加工作线程,但这时如果超时没有获取到任务,也就是 timedOut 为 true 的情况,说明 workQueue 已经为空了,也就说明了当前线程池中不需要那么多线程来执行任务了,可以把多于 corePoolSize 数量的线程销毁掉,保持线程数量在 corePoolSize 即可

什么时候会销毁?getTask 方法返回 null 时,在 runWorker 方法中会跳出 while 循环,然后会执行processWorkerExit 方法。processWorkerExit 方法会将这个worker从workers中移出。之后由JVM自动回收。

shutdown/shutdownNow

ThreadPoolExecutor 提 供 了 两 个 方 法 , 用 于 线 程 池 的 关 闭 , 分 别 是 shutdown() 和shutdownNow(),其中:

  • shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
  • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

五、配置线程池的大小

如何合理配置线程池大小,线程池大小不是靠猜,也不是说越多越好。
在遇到这类问题时,先冷静下来分析

  1. 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
  2. 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系

如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,假如 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1。
如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍

一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/ 线程 CPU 时间 )* CPU 数目

这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner测试大量运行次数求出平均值)

参考链接:Java线程池七个参数详解
参考链接:ThreadPoolExecutor源码解析
参考链接:彻底理解Java线程池原理篇
参考链接:ThreadPoolExecutor源码剖析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章