JDK源码学习1-ThreadPoolExecutor学习,先看注释

写在开篇

线程池的源码从刚开始学Java就在看,刚开始看得很痛苦,纵然师父给我手把手讲过一遍,我依然是半懂半不懂。如今距离刚开始学Java过去一年了,可能一方面是本身对Java语言愈来愈熟,另外一方面是用到了线程池的相关知识,再来看源码,已经没那么吃力了java

一点心得:编程

JDK的源码是必定要看的,只要你学Java。这里的看不仅是跟着我或者其余人的博文看过一遍就算看了,是本身要硬生生去亲自啃这块骨头。为何呢?由于“横当作岭侧成峰”,同一本书,同一段代码在不一样的人的眼中,内容是不一样的。写博客的人会着重讲本身认为重要的,忽略掉一些不重要的部分,而可能对于初学者的你或者我,这些“不重要”的部分,咱们是不懂的。可是也不意味着看别人博文没意思,有些博文确实e......可是用心写的博文,通常加入了做者的思考和经验,这些都是无形的财富。并发

说这些不是要你一开始就去啃这块难啃的骨头,最近在看《软技能》,对里面的学习法感同身受。学习新知识并非一开始就要看很深刻的东西,而是先了解最简单的,基础,去用,不会再看,再用。好比学习线程池,咱们先了解什么是线程池,有什么好处,有什么内容,怎么用。再结合我或者他人的博客去看看源码,再本身去看看源码,当本身能输出(博文,或者讲给其余人听)的时候,就算懂了。异步

了解ThreadPool,必定要看Doug Lea大神的注释,私觉得不少博客写的还不如大神的注释,本节内容基本是注释原文翻译,括号内是本人加入的一些补充。下一节有读源码的我的经验和源码的解读。函数

1. 线程池功能

线程池解决了2个问题:源码分析

  1. 在执行大量异步任务时,经过减小每一个任务的调用开销,提升了性能(不用重复建立销毁线程等)。
  2. 它提供了能够管理并限制资源的方法,这些资源包括线程(限制线程池的大小,队列大小等等)。

    每一个线程池都维护了一些基础的统计信息,例如完成的任务数。性能

2. 可调节的参数和钩子

2.1 线程池的core size和max size:

线程池能够经过core sizemax size动态调节池的大小(线程数的多少)。学习

当一个新任务经过 execute(Runnable)提交时,若是工做线程数 < core size,那么线程池会新建一个线程来处理这个新任务,即便这些工做线程处于空闲的状态(也就是没有处理任务)。线程

若是工做线程数 > core size,可是 < max size,那么这个任务会被塞入队列,除非队列满了。翻译

若是设置core size=max size,那么实际上,你建立了一个固定大小的线程池

若是设置max size 为无限大,好比Integer.MAX_VALUE,那么这个线程池能够同时处理任意多个任务。

一般状况下,core sizemax size在初始化的时候就设置好了。固然,你能够随时经过setCorePoolSizesetMaximumPoolSize方法更改。

2.2 根据需求进行初始化

默认状况下,当新任务到达时,工做线程才会被建立。可是你能够经过prestartCoreThread或者prestartAllCoreThreads方法预先让core size个线程提早启动。当你初始化的时候,若是传入的队列是非空的(也就是已经有任务“火烧眉毛”地待执行),这个时候,你须要提早准备好运行的线程(具体查看下一节的源码分析,就知道缘由了)。

2.3 新建线程

新线程由ThreadFactory工厂建立,若是你没有提供,线程池将使用Executors#defaultThreadFactory,也就是默认的工厂类来构造。经过默认工厂建立的线程都在一个线程组(thread group)里,他们拥有一样的“NORM_PRIORITY”优先级和“非守护线程”的配置。若是你提供了不一样的工厂,你能够修改线程的名字,线程组,优先级,守护状态等等。

当工厂新建线程失败,池会继续运行,可是可能无法处理任何任务。线程应该拥有名为“modifyThread”的运行时权限(RuntimePermission).若是工做线程,或者其余线程使用这个池,可是没有拥有这个权限,服务可能会退化:配置虽然修改了,可是没有及时起效,而且一个关闭(SHUTDOWN)的池可能处于终止但可能未完成的状态。

2.4 Keep-alive 时间

​ 若是如今线程池的线程数量 > core size ,其中某个线程的空闲时间(一直没有拿到任务) > keep-alive time,那么这个线程会终止。这个方式能够在线程池没有太多任务的时候,用来下降线程资源的消耗。 keep-alive time能够经过setKeepAliveTime(TimeUnit)方法动态更改。若是传入Long.Max_VALUE,那么空闲线程将永远不会被终止。默认状况下,只有线程数超过core size, 超时策略才会生效。但allowCoreThreadTimeOut(boolean)方法可让超时策略在线程数小于 core size时候也生效,只要keep-alive time非0。

2.5 入队

任何的阻塞队列能够传递和接收任务,可是具体策略和线程池的大小有关:

  1. 若是线程数 < core size,线程池会新建线程来处理新任务,而不会塞入队列。
  2. 若是线程数 >= core size,新任务会入队。
  3. 若是新任务入队失败(例如队列满了),而且线程数量 < max size,那么会新建线程处理任务。反之若是线程数= max size,那么任务会被拒绝。

线程池的状态贯穿了线程池的整个生命周期,有如下5个生命周期:

RUNNING: 接收新任务,处理队列的任务。

SHUTDOWN:不接收新任务,继续处理队列的任务。

STOP:不接收新任务,也不处理队列里的任务,并尝试中止正在运行的任务。

TIDYING:全部的任务都终止了,线程数为0以后,线程池状态会过分到TIDYING,而后执行terminated()钩子方法。

TERMINATED:在terminated()方法执行完以后,线程池状态就会变成TERTMINATED。

每一个状态对应的数值很重要,用于后续的比较,每一个状态的数值递增,但状态的变化并不须要连贯。有如下几种变化形式:

RUNNING -> SHUTDOWN:当调用 shutdown()或者 finalize()方法时

(RUNNING or SHUTDOWN) -> STOP:调用 shutdownNow()方法时

SHUTDOWN -> TIDYING:当队列和池都空了的时候

STOP -> TIDYING:当池空了

TIDYING -> TERMINATED:当 terminated() 方法结束的时候。

线程池有如下三个入队策略:

a. 直接传递:

一个优秀的默认队列是SynchronousQueue.它会在任务入队后,马上将任务转给线程处理,而不保留任务。若是没有可用的线程(无法新建更多的线程)来处理新任务,那么会入队失败。这个策略能够避免任务被锁住(线程的饥饿死锁,查看页尾补充说明)

b. 无界队列:

在线程池中使用无界队列(例如没有预设容量的LinkedBlockingQueue),意味着池中若是有core size个线程正在运行,那么新来的任务会所有塞入这个队列。所以,该线程池最多存在core size个工做线程(max size将会失效)。当任务彼此不相关时,这是一个很好的作法。例如,无界队列能够容纳突如其来的顺势爆发的请求,即便请求到来的速度超出服务的处理速度。

c. 有界队列:

有界队列(例如 ArrayBlockingQueue)能够经过设定max size来保护资源,但同时也更难协调和控制。队列的长度和池的大小须要相互协调:

​ 长队列和小(线程池)池的组合减小了CPU的使用,OS 资源和上下文切换带来的损耗,可是可能会人为地下降吞吐量。若是任务常常阻塞(例如I/O密集型任务),系统能够为更多的线程安排时间,可能比你设定的线程数还要多(没有充分利用CPU)。

短队列一般须要和大(线程)池搭配使用,它们能充分利用CPU,可是也可能会带来不可预计的调度开销,于是下降吞吐量。

2.6 拒绝任务

当线程池SHUTDOWN以后,或者在设定了固定的池的最大线程数和队列长度,并都处于饱和的状态下,经过execute(Runnable)方法提交的任务会被拒绝。在上述两种状况下,execute方法会调用RejectedExecutionHandler#rejectedExecution(Runnable,ThreadPoolExecutor)方法,RejectedExecutionHandler是一个接口,每一个线程池的RejectedExecutionHandler变量不同,该接口有四种具体实现:

  1. ThreadPoolExecutor.AbortPolicy(默认):拒绝新任务,并抛出RejectedExecutionException异常。
  2. ThreadPoolExecutor.CallerRunsPolicy : 调用execute方法的线程自己来执行这个任务。这种作法提供了一个简易的反馈控制机制下降新任务的提交频率。
  3. ThreadPoolExecutor.DiscardPolicy:直接丢弃。
  4. ThreadPoolExecutor.DiscardOldestPolicy:线程池正常运行的状况下,放弃最旧的未处理请求,而后重试 execute;若是执行程序已关闭,则会丢弃该任务。

固然,你也能够自定义其余的拒绝策略。这时你须要格外当心,尤为在你的策略应用于特定的池的大小,或者排队策略上时。

2.7 钩子方法

ThreadPoolExecutor类提供 beforeExecute(Thread, Runnable)

afterExecute(Runnable, Throwable)} 两个可被覆盖的钩子函数。它们在任务的开始和结束的时候被调用。可被用于配置运行环境,例如更改ThreadLocals,收集统计信息,或者加日志。此外,terminated()方法也能够被覆盖,在线程池彻底终止的时候,你能够经过这个方法作一些特殊的处理。

若是钩子方法抛出异常,内部的工做线程可能会逐个失败直至线程池终止

2.8 队列维护

getQueue()能够获取队列来监控和调试,强烈不建议你们使用这个方法来达到其余目的。当大量的入队任务被取消时,remove(Runnable)purge方法能够帮助来回收空间。

2.9 回收线程池

当一个线程池再也不被其余程序引用,而且池中没有线程的时候,就会自动shut down。若是你但愿一个再也不被引用的线程池能够被自动回收(都说是自动,固然不是手动使用shutdown方法),那么你必须确保空闲线程会自动中止。你能够经过设置keep-alive time,core size设为0,而且要记住调用allowCoreThreadTimeOut方法使keep-alive time在全部线程上都能生效。

3 用例

这是一个使用线程池的例子,咱们新增了一个简单的中止/恢复 功能:

class  PausableThreadPoolExecutor extends ThreadPoolExecutor {
    private boolean isPaused;
    private ReentrantLock pauseLock = new ReentrantLock();
    private Condition unpaused = pauseLock.newCondition();
    public PausableThreadPoolExecutor(...) { super(...); }
    
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
         pauseLock.lock();
        try {
            while (isPaused) 
                unpaused.await();
        }  catch (InterruptedException ie) {
            t.interrupt();
        }  finally {
            pauseLock.unlock();
        }
    }

    public void pause() {
        pauseLock.lock();
        try {
            isPaused = true;
        }  finally {
            pauseLock.unlock();
        }
    }

    public void resume() {
        pauseLock.lock();
        try {
            isPaused = false;
            unpaused.signalAll();
        }  finally {
            pauseLock.unlock();
        }
    }
}

4. 补充说明

1.线程饥饿死锁(《Java并发编程实战》):

在线程池,若是任务依赖于任务,那么可能产生死锁。在单线程的Executor中,若是一个任务将另外一个任务提交到同一个Executor,而且等待这个被提交的结果,那么一般会发生死锁。若是正在执行的线程都因为等待其余仍处于工做队列的任务而阻塞,这种现象称为饥饿死锁(Thread Starvation Deadlock)。只要线程池中的的任务,须要无限期等待一些必须由池中其余任务才能提供的资源,或者条件,例如某个任务等待另外一个任务的返回值或者执行结果,那么除非这个池够大,不然将发生线程饥饿死锁。

相关文章
相关标签/搜索