【多线程与并发】线程池

一、前言

1. 什么是线程池

线程池是一个统一管理线程的工具,顾名思义它如同一个池子一般装载了创建出来的线程,并掌控着它们的生命周期和调度工作,从而起到复用线程的作用。这种池化的思想还常常用于数据库连接。在JDK中线程池对应的类为java.util.concurrent.ThreadPoolExecutor

2. 为什么需要线程池

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗(创建销毁线程需要系统调用)。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

3. 线程池架构设计

下图是线程池的继承关系层级。处于顶层的是Executor接口,该接口只有一个方法execute,该方法解耦了线程(Thread)和任务(Runnable),使调用者无需关心线程的状态。

ExecutorService接口则扩展了Executor,主要增加了提交带返回值任务(Callable)的方法以及关闭线程池的方法,例如shutdown和shutdownNow。

AbstractExecutorServiceThreadPoolExecutor则是这两个接口的实现类。AbstractExecutorService主要实现Callable任务的提交,ThreadPoolExecutor主要实现Runnable任务的执行以及线程池的关闭
线程池类图

二、线程池如何创建

ThreadPoolExecutor有两种创建方式,第一种是使用new显式的创建,第二种是使用Executors的工厂方法创建。不过在阿里巴巴开发手册中并不推荐使用第二种方式,原因我们下面再说。

1. 使用new创建

使用new关键字创建线程池前,我们先来了解一下构造方法的七大参数。

参数 含义
int corePoolSize 线程池的核心线程数量
int maximumPoolSize 线程池的最大线程数量
long keepAliveTime 线程池的非核心线程(maximumPoolSize-corePoolSize)的存活时间,当非核心线程没有任务可以执行时开始计时
TimeUnit unit 存活时间的时间单位,例如秒、分、时等
BlockingQueue<Runnable> workQueue 存放任务的阻塞队列,当核心线程都在执行任务时新提交的任务进入队列缓存
ThreadFactory threadFactory 线程池的线程工厂,用于指定线程的一些属性,例如名字。
RejectedExecutionHandler handler 当阻塞队列已满,且线程池的线程数已到达最大值的拒绝策略

第一个参数corePoolSize指定了能够常驻线程池的线程数量,即使任务非常空闲,线程池也总是能够保存小于或等于该数目的线程。为什么是<=而不是=呢?因为ThreadPoolExecutor使用的是懒加载的方式创建线程,换句话说当你new出ThreadPoolExecutor时并不会直接创建线程,而是只有当第一个任务到达时才会创建出来,之后才会常驻于线程池。

第二个参数maximumPoolSize是线程池的最大线程数量,当线程池的任务队列满时且核心线程繁忙时才会用到这个参数。因为这个参数也产生出非核心线程的概念,它等于最大线程数-核心线程数。不过这只是一个人为的概念而已,线程池并不会给创建出来的线程贴上核心非核心的标签,而是仅仅根据core和max的数目来控制线程的创建和销毁。

第五个参数是阻塞队列,当核心线程繁忙时用于缓存任务,等核心任务空闲时取用。JDK提供了如下几个常用阻塞队列

队列 描述
ArrayBlockingQueue 用数组实现的有界阻塞队列,创建时必须指定队列大小
LinkedBlockingQueue 用链表实现的阻塞队列,可以在创建时指定队列大小,若不指定则长度为Integer.MAX_VALUE
SynchronousQueue 一个不存储元素的阻塞队列,每一个put必须等待take,每一个take也必须等待put

第六个参数是线程工厂,在阿里巴巴java开发手册中强制开发者自定义线程工厂指定创建出来的线程名称,方便出错时回溯。

第七个参数是拒绝策略,JDK提供了以下几个拒绝策略

拒绝策略 描述
AbortPolicy 丢弃新增的任务,并且抛出RejectedExecutionException异常
DiscardPolicy 丢弃新增的任务,不抛出异常
DiscardOldestPolicy 丢弃队头任务,重新提交被拒绝的任务
CallerRunsPolicy 由调用者线程执行该拒绝的任务

2. 使用Executors工具类创建

JDK的类命名非常规范,通常复数类是单数类的工具类。例如Objects和Object,Collections和Collection,Arrays和数组对象。同理Executors工具类有如下几个典型的工厂方法用来创建线程池:

  1. Executors.newFixedThreadPool(int nThreads)
    该方法创建出一个具有固定线程数目的线程池,源码也是通过new的方式创建new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())可以看到任务队列的长度是无界的(严格来说长度是Integer.MAX_VALUE,不过可以忽略该上限),会导致OOM的出现

  2. Executors.newSingleThreadExecutor()
    该方法创建出一个单线程的线程池,源码是new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()),同样的可以看到无界的任务队列会导致OOM的出现

  3. Executors.newCachedThreadPool()
    该方法创建的是一个缓存线程的线程池,源码是new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()),虽然有60s的线程缓存时间,但是最大线程数目无上限,所以面对洪峰也有导致OOM的可能

三、线程池状态

线程池有如下五个状态:

状态 描述
RUNNING 运行状态,线程池能接受新提交的任务,并且也能处理阻塞队列中的任务
SHUTDOWN 优雅关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务,线程池会调用interrupt()方法中断空闲的线程,调用shutdown()方法进入该状态
STOP 停止状态,不能接受新任务,也不处理队列中的任务,线程池会调用interrupt()方法中断正在处理任务的线程,使用shutdownNow()进入该状态
TIDYING 清理状态,所有的任务都已终止了,workerCount (有效线程数) 为0
TERMINATED 终止状态,在terminated() 方法执行完后进入该状态,不过默认terminated()方法中什么也没有做,留给子类覆写

下图展示了线程池状态如何变换流转
线程池状态
下面来看下源码是如何控制这些状态的,可以看到源码中充斥着大量的位运算,让人不得不感慨JUC包作者Doug Lea的深厚功力。虽然位运算对于计算机来说是非常高效的,不过对于阅读者来说就不是那么友好了,第一次读非常的费劲难以理解。

// java.util.concurrent.ThreadPoolExecutor

// ctl为线程池状态(runState)和线程数量(workerCount)合一的变量,初始状态为RUNNING,线程数量为0
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// 该变量表示workerCount的位数。int类型大小为32位,则COUNT_BITS=32-3=29,即ctl中线程数占29位状态占3位 
private static final int COUNT_BITS = Integer.SIZE - 3;

/* 
该变量表示workerCount支持的最大值。既然线程数占29位,那么容量为2^29,最大值为2^29-1
代码的含义是1左移29位再减一,用数学表达式就是1*2^29-1=536870911,也就是五亿多个
从二进制的角度来看,该变量表示高位3个0低位29个1
*/
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

/*
-1在计算机中使用补码的形式表示,二进制比特数为 1111 1111 1111 1111 1111 1111 1111 1111
左移29位可得到						   1110 0000 0000 0000 0000 0000 0000 0000

0的二进制比特数为 						   0000 0000 0000 0000 0000 0000 0000 0000
左移29位可得到						   0000 0000 0000 0000 0000 0000 0000 0000

1的二进制比特数为						   0000 0000 0000 0000 0000 0000 0000 0001
左移29位可得到						   0010 0000 0000 0000 0000 0000 0000 0000

2的二进制比特数为						   0000 0000 0000 0000 0000 0000 0000 0010
左移29位可得到						   0100 0000 0000 0000 0000 0000 0000 0000

3的二进制比特数为						   0000 0000 0000 0000 0000 0000 0000 0011
左移29位可得到						   0110 0000 0000 0000 0000 0000 0000 0000

可以发现左移后runState都移到了高三位,所以ctl的高三位是runState,低29位为workerCount
*/
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;

/*
这边的入参c代表的就是ctl,由于该变量是二合一变量,所以当需要获取其中一者的值的时候,必须对其进行拆解。
runStateOf方法利用CAPACITY的取反将高3位置为1,低29位置为0,再和ctl进行与运算就可以还原出高3位的状态值,忽略低29位的数量值
workerCountOf方法将高3位为0,低29位为1的CAPACITY和ctl进行与运算就可以还原出低29位的数量值,忽略高3位的状态值
ctlOf则利用状态的低29位0,数量的高3位0,进行或运算将两个变量合二为一,非常巧妙

举个例子,一个线程池,线程数量为3,状态为RUNNING.则相关变量值如下
workerCount: 0000 0000 0000 0000 0000 0000 0000 0011
runState:	 1110 0000 0000 0000 0000 0000 0000 0000
CAPACITY:	 0001 1111 1111 1111 1111 1111 1111 1111
~CAPACITY:	 1110 0000 0000 0000 0000 0000 0000 0000

ctl = ctlOf(runState, workerCount) = runState|workerCount = 1110 0000 0000 0000 0000 0000 0000 0011
runStateOf(ctl) = ctl & ~CAPACITY = 1110 0000 0000 0000 0000 0000 0000 0000
workerCountOf(ctl) = ctl & CAPACITY = 0000 0000 0000 0000 0000 0000 0000 0011
*/
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

四、线程池工作流程

1. 提交任务:execute(Runnable command)方法

execute方法是调用者提交任务的关键方法,也是线程池管理任务的核心代码。

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        // 当线程数量小于核心数量时,增加核心线程并使其执行提交的任务。需要注意的是线程池初始线程数为0而不是corePoolSize
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            // 增加核心线程失败,说明此时有两种可能:1)并发场景下,线程数量已经等于corePoolSize了 2)线程池关闭了
            c = ctl.get();
        }
        /*
        当
        1)增加核心线程失败(上一个if判断没有退出该方法)
        2)或者当前线程数量大于等于核心线程数目(上一个if判断没有进入),且线程池处于RUNNING态,
        则将任务放入任务队列
        */
        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);
        }
        // 当线程数大于等于核心线程数,且任务队列已经饱和(workQueue.offer(command)返回了false),则尝试增加非核心线程并将任务交予执行
        else if (!addWorker(command, false))
        	// 当线程数已经等于maxPoolSize(addWorker(command, false)返回了false)则拒绝该任务。至于如何拒绝则根据具体策略(RejectedExecutionHandler)决定
            reject(command);
    }

2. 封装线程:Worker类

前面说了这么多worker,那么它究竟是什么呢?其实它是线程池对线程的一个封装,继承了AQS并且实现了Runnable接口。继承AQS主要是为了实现worker状态的切换,AQS的state字段在这里代表着worker是否空闲,0为空闲,1为正在执行任务。这个状态是线程池shutdown时对worker是否interrupt的一个重要依据,当worker的state为0时,线程池关闭可以中断它使其正常退出,当state为1时则等待线程执行完当前任务实现优雅关闭。

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    private static final long serialVersionUID = 6138294804551838833L;
    final Thread thread;  // 该线程所要执行的runnable任务就是自身,是通过线程工厂创建出来的最后执行任务的线程
    Runnable firstTask;  // 创建该worker时传入的任务,可以为空	
    volatile long completedTasks;  // 该线程完成的任务数目

    Worker(Runnable firstTask) {
        setState(-1); // 初始值为-1,防止该worker被中断,直到runWorker执行
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this); //获取入参传入的线程工厂创建线程
    }

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

    protected boolean isHeldExclusively() {
        return getState() != 0;
    }

    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }
	// lock可以看做将自身状态翻转为忙碌(1),unlock可以看做将自身状态翻转为空闲(0)
    public void lock()        { acquire(1); }
    public boolean tryLock()  { return tryAcquire(1); }
    public void unlock()      { release(1); }
    public boolean isLocked() { return isHeldExclusively(); }

    void interruptIfStarted() {
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
            try {
                t.interrupt();
            } catch (SecurityException ignore) {
            }
        }
    }
}

3. 增加线程:addWorker(Runnable firstTask, boolean core)方法

可以看到在execute方法中频繁地调用addWorker方法,来看下线程池是如何添加线程

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    // 外层循环检查运行状态
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 仅有如下两个情况可以继续往下执行否则不允许增加线程 1)runState为RUNNING 2)当runState为SHUTDOWN时,firstTask==null且任务队列不为空。
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
            
		// 内层循环检查线程数量
        for (;;) {
            int wc = workerCountOf(c);
            // 核心线程数超出corePoolSize不允许增加核心线程;或者总线程数超出最大值,不允许增加非核心线程
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
             // 通过cas的方式增加线程数,成功则退出循环
            if (compareAndIncrementWorkerCount(c))
                break retry;
            // cas增加失败说明有冲突产生(可能由于runState也可能由于workerCount),通过循环重新尝试
            c = ctl.get();  // Re-read ctl
            // 若冲突产生在runState则进入外层循环重新检查。
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
    	// 新增一个Worker,Worker能看做是一个线程
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
        	// 这里使用锁是因为存放woker的集合是hashSet,非线程安全
            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();
                        // 将线程增加到hashset中
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                	// 启动线程
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

4. 执行线程:runWorker(Worker w)方法

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // 将worker状态从初始值-1置为0,从而允许中断
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {  // 循环获取任务
            w.lock(); // 将worker状态置为忙碌(1)
            // 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();  // 若线程池处于STOP状态,则中断该线程
            try {
                beforeExecute(wt, task);  // 该方法默认没有实现,需要子类覆写
                Throwable thrown = null;
                try {
                    task.run();  // 这个task就是用户传进来的任务
                } 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);
    }
}

5. 线程获取任务:getTask()方法

在第四步中可以看到while循环中有个getTask方法,该方法可以说是线程池复用线程的关键,它利用阻塞队列的出队方法巧妙地使线程在没有任务的情况下进入阻塞状态,然后当任务到来时再利用while自旋实现线程循环获取runnable任务执行

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 若线程池停止则减去该线程
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // 通过判断allowCoreThreadTimeOut以及线程数是否大于corePoolSize,决定该线程是否有限期获取任务
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            // 线程数超出最大限制或者取任务超时,利用cas减去线程数目,如果返回null可使线程退出while循环,从而结束生命周期
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
        	// 默认情况下核心线程无限期等待任务到来,非核心线程有限期(期限为keepAliveTime)等待任务,等待不到就返回null
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

6. 线程退出: processWorkerExit(Worker w, boolean completedAbruptly)

当线程执行完runWorker退出后,会进入该方法,该方法会根据线程池的当前状态进行清理或者维护操作。

    private void processWorkerExit(Worker w, boolean completedAbruptly) {
    	// 若线程是抛出异常退出的这里需要将线程数减一,如果是正常退出的则在getTask方法中已经减一过了
        if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
            decrementWorkerCount();

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            completedTaskCount += w.completedTasks;
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }
        
		/* tryTerminate主要是看(1)线程池是否处于SHUTDOWN/STOP状态(2)且线程池数目是否为0,
		如果满足这两个条件则将线程池状态翻转为TIDYING,
		然后执行terminated方法并将状态翻转为TERMINATED。
		这个方法就是上面线程池状态流转图的实现,可以结合图片一起看下。
		*/
        tryTerminate();

		/*
		当线程是RUNNING或SHUTDOWN状态的情况下:
		1. 若线程异常退出,则新增一个线程
		2. 该线程退出后若核心线程小于corePoolSize(或1)且任务队列还有任务,则重新补一个线程回来
		*/
        int c = ctl.get();
        if (runStateLessThan(c, STOP)) {
            if (!completedAbruptly) {
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            addWorker(null, false);
        }
    }

7. 关闭线程池:shutdown()

执行完shutdown后,线程池还会继续执行当前任务和阻塞队列中的任务

 public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        // 通过主锁保证关闭操作与其他操作(例如增加线程)隔离开,实现串行化
        mainLock.lock();
        try {
        	// 安全策略判断
            checkShutdownAccess();
            // cas的方式将状态翻转为SHUTDOWN
            advanceRunState(SHUTDOWN);
            // 中断闲置线程,所谓的闲置线程就是阻塞等待任务队列的那些线程,该方法通过tryLock判断,而tryLock又是通过worker当前的state是否为0来判断的
            interruptIdleWorkers();
            // 一个钩子方法,默认没有实现,ScheduledThreadPoolExecutor子类覆写了该方法
            onShutdown(); 
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

8. 停止线程池:shutdownNow()

与shutdown的区别就是,该方法会中断正在执行任务的线程,并且清空任务队列

public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        // 通过主锁保证停止操作与其他操作(例如增加线程)隔离开,实现串行化
        mainLock.lock();
        try {
	        // 安全策略判断
            checkShutdownAccess();
            // cas的方式将状态翻转为STOP
            advanceRunState(STOP);
            // 与shutdown不同的是,这里是中断所有线程池中的线程
            interruptWorkers();
            // 清空任务队列
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

五、参考

Java线程池实现原理及其在美团业务中的实践
深入理解Java线程池:ThreadPoolExecutor

猜你喜欢

转载自blog.csdn.net/hch814/article/details/106898593