线程池源码详细解读(上)

前文回顾

  • AQS源码详细解读
  • ReentrantLock源码详细解读
  • LinkedBlockingQueue源码详细解读

线程池里用到了阻塞队列,修改ctl状态需要一个mainLock,阻塞队列基于入队锁和出队锁,而ReentrantLock的公平锁与非公平锁都是对AQS的进一步实现,不清楚的小伙伴赶紧看看吧~

本篇文章主要讲线程池概述,力求理清相关知识点,尽量保持思路清晰,请耐心看完~

文章导读

  • 线程池的继承树及相关类的介绍
  • ThreadPoolExecutor的工作状态(概述,原理,转换)
  • ThreadPoolExecutor的重要成员变量
  • 盘点一下常见的线程池
  • 总结

一、线程池的继承树及相关类的介绍

首先抛出继承关系图,打开ThreadPoolExcutor配合idea的ctrl+alt+u或ctrl+H食用最佳。

线程池源码详细解读(上)_第1张图片

先把重要接口和类过一遍,混个眼熟。可以看出,Executor是顶级接口,ExecutorService是第一步实现,主要提供了管理线程池的方法。AbstractExecutorService的出现很容易联想到接口适配器,用于对接口抽象化。

左侧的ForkJoinPool基于工作-窃取算法是1.7开始出现的。ThreadPoolExecutor则是最常用的线程池。
下面逐个来分析一下~

Executor

线程池顶级接口,用于启动线程任务。
void execute(Runnable command);

ExecutorService

提供了线程池生命周期的管理方法
定义了线程池的状态:Running,ShuttingDown,Terminated
主要方法罗列一下:

    //优雅关闭,不是强行关闭线程池,回收线程池中的资源,
    //而是不再处理新的任务,将已接收的任务处理完毕后再关闭。
    void shutdown();
    //是否已经关闭,相当于回收了资源
    boolean isShutdown();
    //是否已经结束,相当于回收了资源
    boolean isTerminated();
    //可以提供线程执行后的返回值
    Future submit(Callable task);
    Future submit(Runnable task);

ThreadPoolExecutor

最常用的线程池,除了ForkJoinPool外,其他常用线程池底层都是使用它实现的。其中FixedThreadPool,CachedThreadPool,SingleThreadPool,ScheduledThreadPool等线程池都是由它实现的,内容比较多,下一个标题讲解。

Excutors

Excutor的工具类,类似Collection和Collections的关系。
可以快速的提供若干中线程池(比如固定容量,无限容量,容量为1的线程池)。
线程池是一个进程级的重量级资源,默认生命周期和JVM一致。当开启线程池后,直到JVM关闭为止是线程池的默认生命周期。如果手工调用shutdown方法,那么线程池执行所有的任务后自动关闭。

Callable

可执行接口,类似Runnable接口,可以启动线程的接口。其中定义的方法是call。call方法的作用和Runnable中的run方法完全一致,call有返回值。

Future

代表未来,也就是线程执行结束后的一种结果,如返回值。获取执行结果的方式是通过get方法获取的。get有两个重载方法,具体的都写在下面啦。

    //查看线程是否结束,call方法是否执行结束
    boolean isDone();
    //获取call方法的返回值,阻塞等待线程执行结束并得到结果
    V get() throws InterruptedException, ExecutionException;
    //获取call方法的返回值,阻塞固定时长,等待线程执行结束后的结果,
    //如果在阻塞时长范围内,线程未执行结束,抛出异常
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

二、ThreadPoolExecutor的工作状态

2.1 工作状态概述

ThreadPoolExecutor提供了对于线程生命周期的控制,规定线程池有5种状态,阅读源码注释,发现使用ctl高3位表示。而它的低29位则表示线程池中的工作线程数。所谓的工作线程数,就是已经被允许start并且不允许被停止的线程。这里先别想这些左移表示是怎么实现的,主要先记住这5种状态,下面会分析具体的运算。

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // 工作状态存储在高3位中
    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;

这个表示和AQS等待对列的waitStatus有点像,只不过AQS以更加直白的方式呈现,这里简单回忆一下:

  • CANCELLED =1 线程被取消了
  • SIGNAL =-1 释放资源后需唤醒后继节点
  • CONDITION = -2 等待condition唤醒
  • PROPAGATE = -3 (共享锁)状态需要向后传播
  • 0 初始状态,正常状态

好了,回归正题,详细看下线程池状态:

  • RUNNING(运行,-1):能够接收新任务,也可以处理阻塞队列中的任务。
  • SHUTDOWN(待关闭,0):不可以接受新任务,继续处理阻塞队列中的任务。
  • STOP(停止,1):不接收新任务,不处理阻塞队列中的任务,并且会中断正在处理的任务。
  • TIDYING(整理,2):所有的任务已终止,ctl记录的“任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,terminated()。对于它的实现,在ThreadPoolExecutor中什么也没做。使用了模板方法模式,和AQS的tryAquire()一样,需要子类实现。如果想在进入TIDYING后做点什么,可以对其进行重载。
  • TERMINATED(终止,3):完全终止,且完成了所有资源的释放。

2.2 分析一下线程池状态的标记原理

  • AtomicInteger ctl =newAtomicInteger(ctlOf(RUNNING,0))
    很明显,ctl被初始化为运行状态并且工作线程数设置为0。
  • COUNT_BIT =29
  • CAPACITY =(1<< COUNT_BITS)-1,表示工作线程的最大值,2^29-1。
  • RUNNING =-1<< COUNT_BITS;
    RUNNING =1110 0000 0000 0000 0000 0000 0000 0000,前3位表示-1的补码。
  • STOP =1<< COUNT_BITS;
    STOP = 0010 0000 0000 0000 0000 0000 0000 0000

可以理解成不看后29位,只把前3位转换成补码。正数的原码,反码,补码相同;负数的补码=反码+1。

2.3 工作状态的具体判定

    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; }
  • intrunStateOf(int c):返回的是线程池工作状态,CAPACITY取反再与运算,结果就是高3位不变,将低29位变为0。
  • workerCountOf(int c):返回工作线程数,由于CAPACITY高3位都是0,所以结果是高3位变0,低29位不变。
  • ctlOf(int rs,int wc):初始化线程池或改变线程池状态时执行,将rs与wc进行或运算,结果刚好是他们拼接后的ctl的值。

2.4. 工作状态的转换

下面是源码中的描述:
RUNNING -> SHUTDOWN:在调用ShutDown()时或隐式调用在finalize()中。
(RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()时。
SHUTDOWN -> TIDYING:当阻塞和队列和工作线程数都为0时。
STOP -> TIDYING:当工作线程数为0。
TIDYING -> TERMINATED:当terminated()钩子方法完成时。

线程池源码详细解读(上)_第2张图片 标图片来源于网络题

 

2.5 品读一下源码设计者的魅力

我觉得线程池源码设计者真的是非常巧妙。利用一个COUNT_BIT将工作状态与工作线程数建立联系,并且是易维护的。

线程池源码详细解读(上)_第3张图片

这段话就很好的解释了他们将两个变量设计成一个变量的用意,用我的渣水平大概翻译一下。
为将他们打包到一个变量中,我们限制了工作线程数为2^29-1,而不是2^31-1,不然的话就无法表示了。如果这在未来会成为一个问题,我们会将ctl变量转换为AtomicLong类型(想的还挺远,当工作线程数超过5亿时,希望Java还能这么火)。然后调整下面的移位/屏蔽函数。直到这种需求出现之前,现在的代码运行起来会更快,使用起来更简单。

总之,把runtimeState与workerCount融合起来效率更高,也便于维护~

三、ThreadPoolExecutor的重要成员变量

3.1 构造方法

就看一个具有代表性的构造方法:

线程池源码详细解读(上)_第4张图片

里面的参数也解释一下吧:

  • int corePoolSize--核心线程数
  • int maximumPoolSize--最大线程数
  • long keepAliveTime--允许线程空闲时间
  • TimeUnit unit--时间对象
  • BlockingQueue workQueue--阻塞队列,上篇文章已经讲的很详细了,就不赘述了。
  • ThreadFactory threadFactory--线程工厂
  • RejectedExecutionHandler handler--任务拒绝策略

3.2 线程工厂(ThreadFactory)

private volatile ThreadFactory threadFactory;

我们可以自定义线程工厂
1)可以设置创建线程时间,统一线程前缀名,优先级,是否为守护线程等信息
2)没有则用默认工厂创建--Executors.defaultThreadFactory()

3.3 拒绝策略(RejectedExecutionHandler)

ThreadPoolExecutor的4个内部类就是拒绝策略的实现。

1)直接抛出异常(AbortPolicy)
默认采用,对拒绝任务抛弃处理,并且抛出RejectedExecutionException异常。

2)使用调用者的线程来处理(CallerRunsPolicy)
如果当前线程池处于运行状态 ,直接使用当前线程执行任务,如果是终止状态,则直接抛弃。

3)直接丢掉这个任务(DiscardPolicy)
对拒绝任务偷偷地抛弃,没有异常信息。查看源码发现他的rejectedExecution ()就是一个空实现。

4)丢掉最老的任务(DiscardOldestPolicy)
对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个任务,然后把拒绝任务加到队列。

3.4 其他重要成员变量

    //线程池一些信息更新时使用,比如largestPoolSize,complectedTaskNum,ctl的状态和线程数更新时。
    private final ReentrantLock mainLock = new ReentrantLock();
    //工作线程集合,Worker里实现了Runnable,封装了thread,用于执行任务
    private final HashSet workers = new HashSet();
    //客户端调用awaitTerminate()时会阻塞,
    //当处于terminate状态后,使用condition.signalAll()通知
    private final Condition termination = mainLock.newCondition();
    //记录线程池运行过程中出现过的最大线程数
    private int largestPoolSize;
    //记录完成的任务数量
    private long completedTaskCount;
    //当线程数小于corePoolSize时,是否允许它也遵循keepAliveTime时间限制
    private volatile boolean allowCoreThreadTimeOut;

四、盘点一下常见的线程池

常见的线程池有:FixedThreadPool,CachedThreadPool,SingleThreadPool,ScheduledThreadPool,ForkJoinThreadPool。

4.1 FixedThread Pool

Executors.newFixedThreadPool()
阻塞队列是LinkedBlockingQueue,线程池默认的容量上限是Integer.MAX_VALUE(其实工作线程数最高也就2^29-1)。
将返回一个核心线程数和最大线程数相等的线程池。

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

使用场景:大多数情况下使用的线程池首选推荐FixedThreadPool。OS和硬件是有线程支持上限的,不能随意的无限提供线程池。
常见的线程池容量:pc-200.服务器-1000~10000
并发处理能力≈线程数*(10~18)

4.2 CachedThreadPool

Executors.newCachedThreadPool()
阻塞队列是SynchronousQueue
核心线程数为0,最大线程数为Integer.MAX_VALUE。
对于新的任务,如果线程池中没有空闲线程,则创建一个新的线程处理任务。

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }

容量管理策略:如果线程池中的线程数量不满足任务执行,每次有新任务无法即时处理的时候,都会创建新的线程。默认线程空闲时间60秒,自动销毁。

使用场景:内部应用或测试应用。内部应用。有条件的内部数据瞬间处理时应用,如电信平台夜间执行数据整理(有把握在短时间内处理完所有工作,且对硬件和软件有足够的信心)。测试应用,在测试的时候,尝试得到硬件或软件最高的负载量,用于提供FixedThreadPool容量的指导。

4.3 SingleThreadPool

Executors.newSingleThreadExecutor()
阻塞队列是LinkedBlockingQueue
最大线程数与核心线程数都是1,使用单个worker线程的Executor。

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
    }

使用场景:保证任务顺序时使用。如游戏大厅中的公共频道聊天,秒杀。

4.4 ScheduledThreadPool

Executors.newScheduledThreadPool()
用于定时完成任务。可以搭配Timer.scheduleAtFixedRate()使用。

scheduleAtFixedRate(Runnable,start_limit,limit,timeunit)

  • runnable--要执行的任务
  • start_limit--第一次任务执行的间隔
  • limit--多次任务执行的间隔
  • timeunit--多次任务执行间隔的时间单位
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

使用场景:计划任务时选用(DelayedQueue)如电信行业的数据整理,每分钟整理,每小时整理,每天整理等。

4.5 ForkJoinThreadPool

Executors.newWorkStealingPool()
采用了工作窃取算法(work-stealing):所有池中线程会尝试找到并执行已被提交到池中的或由其他线程创建的任务。这样很少有线程会处于空闲状态,非常高效。这使得能够有效地处理以下情景:大多数由任务产生大量子任务的情况;从外部客户端大量提交小任务到池中的情况。在实现上,每个工作线程都有自己的任务队列,每次都先找其他工作线程的底部(base)任务,完成后再从顶部(top)开始完成自己的任务。

ForkJoinTask类型提供了两个抽象子类型,RecursiveTask(有返回结果的分支合并任务),RecursiveAction(无返回结果的分支合并任务,可当成Callable与Runnable理解)。

ForkJoinThreadPool没有所谓的容量,默认都是一个线程,根据任务自动的分支新的子线程。当子线程任务结束后,自动合并。所谓自动是根据fork和join方法实现的。

使用场景:主要是做科学计算或天文计算,数据分析。

总结

量太多了,就到这里,好好消化一下。用到的设计模式有工厂模式,模板方法模式,接口适配器模式。读源码看英文注释真的很重要,其次最重要的就是工作状态与拒绝策略。对于工作线程的控制,还有具体的调度,改变量加lock等一系列源码就放在下次吧。
最后思考一下,如何设计一个线程池?

文章若有不当之处,欢迎评论指出~
如果喜欢我的文章,欢迎关注知乎专栏Java修仙道路~

你可能感兴趣的:(并发)