线程池分析

255 阅读8分钟

前言

在部分老项目里,你可能需要对线程的使用进行优化,将以前使用Executor创建的四种类型线程池以及单独new Thread方式就行优化管理。最近刚好在对老项目的优化,采用的方案就是众所周知的全局线程池管理类,利用ThreadPoolExecutor来创建线程池。

线程生命周期相关可了解 线程优化需要了解的一些点

线程池中的CAS部分可了解 CAS、Synchronized、ReentrantLock原理

目录

 

一、线程池简介

ScheduledExecutorService 继承自 ExecutorService,增加了定时任务相关方法

ForkJoinPool 是一种支持任务分解的线程池,一般要配合可分解任务接口 ForkJoinTask 来使用,最适合的是计算密集型的任务

方便开发者使用线程池,JDKExecutors中提供了不同的静态方法给我们使用,如下

newFixedThreadPool

  • 创建固定长度的、可重用的线程池,如果运行中的某个线程由于发生了未预期的Exception而结束,那么线程会补充一个新的线程

newSingleThreadExecutor

  • 创建一个单线程的Executor,如果这个线程异常结束,会创建另一个线程来替代,能确保依照任务在队列中的顺序来串行执行,即任务先进先出的规则执行

newCachedThreadPool

  • 创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程

newScheduledThreadPool

  • 创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似Timer(在阿里巴巴Java规范中Timer已不建议使用,因为单个任务异常会导致后续任务都不能执行)

在阿里Java规范中已经不建议使用Executors创建线程池了,那这是为什么呢?我们先了解原理。

二、线程池工作原理

1、ThreadPoolExecutor的构造方法

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

corePoolSize

核心线程数量

maximumPoolSize

线程池最大能够容纳同时执行的线程数,必须大于或等于 1。如果和 corePoolSize 相等即是固定大小线程

keepAliveTime

非核心线程的超时时长,线程池中的线程空闲时间,当空闲时间达到此值时,线程会被销毁直到剩下 corePoolSize 个线程。

如果ThreadPoolExecutorallowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长。

unit

用来指定 keepAliveTime 的时间单位,有 MILLISECONDSSECONDSMINUTESHOURS

workQueue

等待队列,BlockingQueue 类型。当请求任务数大于 corePoolSize 时,任务将被缓存在此 BlockingQueue

threadFactory

线程工厂,线程池中使用它来创建线程,如果传入的是 null,则使用默认工厂类 DefaultThreadFactory

handler

执行拒绝策略的对象。当 workQueue 满了之后并且活动线程数大于 maximumPoolSize 的时候,线程池通过该策略处理请求

2、常用阻塞队列

阻塞队列:队列已满,放元素被阻塞。队列为空时,拿数据会被阻塞。

在生产者、消费者模型中,为了防止生产者或者消费者生产、消费过快,一般会使用阻塞队列实现。

ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列

LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列

PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列

SynchronousQueue:一个不存储元素的阻塞队列

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

DelayQueue:一个使用优先级队列实现的无界阻塞队列

3、ThreadPoolExecutor.execute

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        //步骤1
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //步骤2
        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);
        }
        //步骤 3
        else if (!addWorker(command, false))
            reject(command);
    }

总共3个步骤

步骤1

  • ctlAtomicInteger类型,二进制高3位用来标识线程池的状态,低29位用来记录线程池中的数量

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static int ctlOf(int rs, int wc) { return rs | wc; }
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int COUNT_BITS = Integer.SIZE - 3;
    

    计算当前运行状态

    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    

    计算当前线程数量

    private static int workerCountOf(int c)  { return c & CAPACITY; }
    

    线程池所拥有的状态如下

        // runState is stored in the high-order bits
        //默认状态,接受新任务并处理排队任务
        private static final int RUNNING    = -1 << COUNT_BITS;
        //不接受新任务,但处理排队任务,调用shutDown方法触发
        private static final int SHUTDOWN   =  0 << COUNT_BITS;
        //不接受新任务,不处理排队任务,中断正在运行的任务,调用shutDowNow方法触发
        private static final int STOP       =  1 << COUNT_BITS;
        //所有任务都已终止,workerCount为0时,线程状态转换到该状态,并执行terminate方法
        private static final int TIDYING    =  2 << COUNT_BITS;
        private static final int TERMINATED =  3 << COUNT_BITS;
    

    shutDown

    线程池状态立即变成SHUTDOWN状态,等任务执行完后才中断线程

    shutDowNow

    线程池立即变为STOP状态,不等任务执行完就中断线程

  • workerCountOf(c) < corePoolSize 即线程池中运行的线程数量还没有达到 corePoolSize大小时,线程池会创建一个新线程执行提交任务,无论之前的线程是否处于空闲状态

    if (workerCountOf(c) < corePoolSize) {
          if (addWorker(command, true))
             return;
          c = ctl.get();
     }
    
  • addWorker方法,注释在代码中,core代表核心线程,如果活动线程数小于设定的corePoolSizecore就为true

        private boolean addWorker(Runnable firstTask, boolean core) {
            retry:
            for (;;) {
                int c = ctl.get();//1.获取线程池运行状态
                int rs = runStateOf(c);
    
                // Check if queue empty only if necessary.
                //2.是否可以添加任务
                if (rs >= SHUTDOWN &&
                    ! (rs == SHUTDOWN &&
                       firstTask == null &&
                       ! workQueue.isEmpty()))
                    return false;
    
                for (;;) {
                    int wc = workerCountOf(c);
                    //3.线程数数量是否大于线程池上限或者核心线程数或者最大线程数
                    if (wc >= CAPACITY ||
                        wc >= (core ? corePoolSize : maximumPoolSize))
                        return false;
                    //4.利用CAS操作来增加线程数的数量
                    if (compareAndIncrementWorkerCount(c))
                        break retry;
                    c = ctl.get();  // Re-read ctl
                    //5.线程数数量没有增加、线程池状态改变了,重新走流程
                    if (runStateOf(c) != rs)
                        continue retry;
                    // else CAS failed due to workerCount change; retry inner loop
                }
            }
    	  //6.一般情况会走到4处,CAS增加线程数量,然后执行下面逻辑
            boolean workerStarted = false;
            boolean workerAdded = false;
            Worker w = null;
            try {
            //7.新建Worker对象
                w = new Worker(firstTask);
                //8.获取当前线程,此线程经过默认ThreadFactory创建的不会是守护线程
                final Thread t = w.thread;
                if (t != null) {
                //8.上锁
                    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());
    				//9.线程池没有关闭状态,不处于不接受新任务状态
                        if (rs < SHUTDOWN ||
                            (rs == SHUTDOWN && firstTask == null)) {
                            if (t.isAlive()) // precheck that t is startable
                                throw new IllegalThreadStateException();
                           //10.workers是HashSet类型,上锁保证了多线程问题
                            workers.add(w);
                            int s = workers.size();
                            if (s > largestPoolSize)
                                largestPoolSize = s;
                            workerAdded = true;
                        }
                    } finally {
                        mainLock.unlock();
                    }
                    if (workerAdded) {
                    //11.添加成功,开启线程进入到就绪状态,等待CPU调度执行
                        t.start();
                        workerStarted = true;
                    }
                }
            } finally {
                if (! workerStarted)
                //12.线程添加失败,从workers中移除w,然后恢复线程数量
                    addWorkerFailed(w);
            }
            return workerStarted;
        }
    

步骤2

  • 步骤1中,如果线程数不小于corePoolSize或者是添加线程对应的Worker对象失败,则会执行到步骤2

  • 主要功能是当前线程池中运行的线程数量已经达到 corePoolSize 大小时,线程池会把任务加入到等待队列中,直到某一个线程空闲了,线程池会根据我们设置的等待队列规则,从队列中取出一个新的任务执行

     if (isRunning(c) && workQueue.offer(command)) {//线程池是RUNNING状态,将任务添加到BlockingQueue队列里面
     			//重新检查线程池的状态,不是RUNNING,直接移除任务,并执行拒绝策略,否则如果线程池数量为0,单独创建线程,而不指定任务
                int recheck = ctl.get();
                if (! isRunning(recheck) && remove(command))
                    reject(command);
                else if (workerCountOf(recheck) == 0)
                    addWorker(null, false);
     }
    
  • 为什么在上面的判断检查过程中,会单独创建一个线程,添加空的任务?

    之前已经把 command 提交到阻塞队列了 workQueue.offer(command) 。所以提交一个空线程,直接从阻塞队列里面取就可以了。Thread创建时传递了Worker作为一个任务,执行时会调用Workerrun方法,run方法内调用了runWorker方法,该方法内通过判断传递的Runnable是否为空,为空则从workQueue中取。

  • 如果corePoolSize = 0 呢?线程池执行哪个方法?如何工作?

    corePoolSize0,肯定是直接执行步骤2了,然后判断线程数数量为0,则会通过addWorker(null,false)来尝试新建一个空线程

步骤3

  • 如果线程数大于 corePoolSize 数量但是还没有达到最大线程数 maximumPoolSize,并且等待队列已满,则线程池会创建新的线程来执行任务

     else if (!addWorker(command, false))
                reject(command);
    
  • 如果线程池不是RUNNING或者offer加入阻塞队列失败,那么开启非核心线程池来处理,启动线程数大于maximumPoolSize,任务就会被拒绝

4、AbstractExecutorService.submit

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

submit方法内部还是调用execute(Runnable runnable)方法,只不过这个Runnable被包装成了RunnableFuture

    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

也就是说被包装的Runnable真实类型是FutureTask,这个就不陌生了吧,它间接实现了RunnableFuture两个接口。

FutureTask

三、线程池总结

创建线程池时使用ThreadPoolExecutor类,来指定核心线程数,和最大线程数。

加入的任务会放到指定的阻塞队列中,然后创建核心线程来执行任务,如果活动线程数大于核心线程数,则任务添加到队列中,等待核心线程数去执行(例如固定数量为3的线程池,提交了10个任务,会先由这3个核心线程执行)。当阻塞队列已满,则开启非核心线程数执行。否则执行拒绝策略。

AbortPolicy

丢弃任务并抛出RejectedExecutionException异常,是线程池的默认拒绝策略,在任务不能再提交的时候,抛出异常,及时上报服务器,反馈业务状态,开发人员及时排查。

DiscardPolicy

丢弃任务,但是不能抛出异常。使用此策略,可能会使我们无法发现系统的异常状态。一般无关紧要的业务采用此策略。

DiscardOldestPolicy

丢弃队列最前面的任务,然后重新提交被拒绝的任务。是否采用此种拒绝策略,根据业务判断。

CallerRunsPolicy

由调用方(提交任务的线程)处理该任务。这种情况是需要让所有任务都执行完毕,适合大量计算任务类型去执行,看实际业务场景,提高吞吐量但是任务全都要执行可选。

Java线程池的核心线程数和最大线程数总是容易混淆怎么办

四、为什么禁止使用Executors

我们直接看Executors创建线程的几个方法

     public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }nousQueue<Runnable>());    }    public ScheduledThreadPoolExecutor(int corePoolSize) {        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,              new DelayedWorkQueue());    }

newSingleThreadExecutornewFixedThreadPool所传递的阻塞队列,都是没有指定大小的,无界,在创建的任务很多情况下,可能会有很多没处理的任务放到队列里面,队列无限大,可能会导致OOM

newCachedThreadPoolScheduledThreadPoolExecutor例外,它是线程数最大值为 Integer.MAX_VALUE。这样又会有什么问题呢?

在任务很多的情况下,会创建很多线程,超出手机对单个进程创建线程的限制,直接OOM

阿里巴巴JAVA规范也有说明,如下图