发神经系列-1:线程池

问题:

1.单机上一个线程正在处理服务,如果忽然断电了怎么办(正在处理和阻塞队列里的请求怎么处理)

2.为什么要使用线程池,线程池用什么用

3.说说几种常见的线程池及使用场景

4.线程池有哪几种工作队列

5.怎么理解无界队列和有界队列

6.线程池中的几种重要的参数及流程

什么是线程池

线程池的概念大家应该都很清楚,帮我们重复管理线程,避免创建大量的线程增加开销。

除了降低开销以外,线程池也可以提高响应速度,了解点 JVM 的同学可能知道,一个对象的创建大概需要经过以下几步:

  1. 检查对应的类是否已经被加载、解析和初始化
  2. 类加载后,为新生对象分配内存
  3. 将分配到的内存空间初始为 0
  4. 对对象进行关键信息的设置,比如对象的哈希码等
  5. 然后执行 init 方法初始化对象

创建一个对象的开销需要经过这么多步,也是需要时间的嘛,那可以复用已经创建好的线程的线程池,自然也在提高响应速度上做了贡献。

java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池

用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

线程池的应用场景

假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。

线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。

线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:

假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。

线程池的组成

一个线程池包括以下四个基本组成部分:

1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;

2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;

3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的                                            收尾工作,任务的执行状态等;

4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

ThreadPoolExecutor类中其他的一些比较重要成员变量:
private final BlockingQueue workQueue;              //任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock();   //线程池的主要状态锁,对线程池状态(比如线程池大小
                                                              //、runState等)的改变都要使用这个锁
private final HashSet workers = new HashSet();  //用来存放工作集
 
private volatile long  keepAliveTime;    //线程存活时间   
private volatile boolean allowCoreThreadTimeOut;   //是否允许为核心线程设置存活时间
private volatile int   corePoolSize;     //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int   maximumPoolSize;   //线程池最大能容忍的线程数
 
private volatile int   poolSize;       //线程池中当前的线程数
 
private volatile RejectedExecutionHandler handler; //任务拒绝策略
 
private volatile ThreadFactory threadFactory;   //线程工厂,用来创建线程
 
private int largestPoolSize;   //用来记录线程池中曾经出现过的最大线程数
 
private long completedTaskCount;   //用来记录已经执行完毕的任务个数

线程池的“真相”

发神经系列-1:线程池_第1张图片


发神经系列-1:线程池_第2张图片

发神经系列-1:线程池_第3张图片

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

发神经系列-1:线程池_第4张图片

submit 方法:

submit 方法会提交一个任务去给线程池执行,该任务可以是带返回结果的 Callable 任务,也可以是一开始就指定结果的 Runnable 任务,或者不带结果的 Runnable 任务(此时即一开始指定结果为 null)。submit 方法会返回一个与所提交任务相关联的 Future。通过 上一篇文章 我们可以知道,Future 的 get 方法可以等待任务执行完毕并返回结果。所以通过 Future,我们可以与已经提交到线程池的任务进行交互。submit 提交任务及任务运行过程大致如下:

  1. 向线程池提交一个 Runnable 或者 Callable 任务;
  2. 将 任务 作为参数使用 newTaskFor 方法构造出 FutureTask;通过 Future 的 get 方法,获得任务的结果。

invokeAll 方法:

invokeAll 方法可以一次执行多个任务,但它并不同等于多次调用 submit 方法。submit 方法是非阻塞的,每次调用 submit 方法提交任务到线程池之后,会立即返回与任务相关联的 Future,然后当前线程继续向后执行;

而 invokeAll 方法是阻塞的,只有当提交的多个任务都执行完毕之后,invokeAll 方法才会返回,执行结果会以List>返回,该 List> 中的每个 Future 是和提交任务时的 Collection> 中的任务 Callable 一 一对应的。带 timeout 参数的 invokeAll 就是设置一个超时时间,如果超过这个时间 invokeAll 中提交的所有任务还有没全部执行完,那么没有执行完的任务会被取消(中断),之后同样以一个 List> 返回执行的结果。


invokeAny 方法:

invokeAny 方法也是阻塞的,与 invokeAll 方法的不同之处在于,当所提交的一组任务中的任何一个任务完成之后,invokeAny 方法便会返回(返回的结果便是那个已经完成的任务的返回值),而其他任务会被取消(中断)。

举一个 invokeAny 使用的例子:电脑有 C、D、E、F 四个盘,我们需要找一个文件,但是我们不知道这个文件位于哪个盘中,我们便可以使用 invokeAny,并提交四个任务(对应于四个线程)分别查找 C、D、E、F 四个盘,如果哪个线程找到了这个文件,那么此时 invokeAny 便停止阻塞并返回结果,同时取消其他任务。


shutdown 方法:

shutdown 方法的作用是向线程池发送关闭的指令。一旦在线程池上调用 shutdown 方法之后,线程池便不能再接受新的任务;如果此时还向线程池提交任务,那么将会抛出 RejectedExecutionException 异常。之后线程池不会立刻关闭,直到之前已经提交到线程池中的所有任务(包括正在运行的任务和在队列中等待的任务)都已经处理完成,才会关闭。


shutdownNow 方法:

与 shutdown 不同,shutdownNow 会立即关闭线程池 —— 当前在线程池中运行的任务会全部被取消,然后返回线程池中所有正在等待的任务。


(值得注意的是,我们 必须显式的关闭线程池,否则线程池不会自己关闭)


awaitTermination 方法:

awaitTermination 可以用来判断线程池是否已经关闭。调用 awaitTermination 之后,在 timeout 时间内,如果线程池没有关闭,则阻塞当前线程,否则返回 true;当超过 timeout 的时间后,若线程池已经关闭则返回 true,否则返回 false。该方法一般这样使用:

  1. 任务全部提交完毕之后,我们调用 shutdown 方法向线程池发送关闭的指令;
  2. 然后我们通过 awaitTermination 来检测到线程池是否已经关闭,可以得知线程池中所有的任务是否已经执行完毕;
  3. 线程池执行完已经提交的所有任务,并将自己关闭;
  4. 调用 awaitTermination 方法的线程停止阻塞,并返回 true

isShutdown() 方法,如果线程池已经调用 shutdown 或者 shutdownNow,则返回 true,否则返回 false


isTerminated() 方法,如果线程池已经调用 shutdown 并且线程池中所有的任务已经执行完毕,或者线程池调用了 shutdownNow,则返回 true,否则返回 false

线程池状态

在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:

volatile int runState;
static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;
static final int STOP       = 2;
static final int TERMINATED = 3;

 

 runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;

  下面的几个static final变量表示runState可能的几个取值。

  当创建线程池后,初始时,线程池处于RUNNING状态;

  如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;

  如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;

  当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。


线程池的处理流程

创建线程池需要使用 ThreadPoolExecutor 类,它的构造函数参数如下:

public ThreadPoolExecutor(int corePoolSize,    //核心线程的数量
                          int maximumPoolSize,    //最大线程数量
                          long keepAliveTime,    //超出核心线程数量以外的线程空余存活时间
                          TimeUnit unit,    //存活时间的单位
                          BlockingQueue workQueue,    //保存待执行任务的队列
                          ThreadFactory threadFactory,    //创建新线程使用的工厂
                          RejectedExecutionHandler handler // 当任务无法执行时的处理器
                          ) {...}

参数介绍如注释所示,要了解这些参数左右着什么,就需要了解线程池具体的执行方法ThreadPoolExecutor.execute:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    //1.当前池中线程比核心数少,新建一个线程执行任务
    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);    //如果创建新线程失败了,说明线程池被关闭或者线程池完全满了,拒绝任务
}
  • 可以看到,线程池处理一个任务主要分三步处理,代码注释里已经介绍了,我再用通俗易懂的例子解释一下:

(线程比作员工,线程池比作一个团队,核心池比作团队中核心团队员工数,核心池外的比作外包员工)

  1. 有了新需求,先看核心员工数量超没超出最大核心员工数,还有名额的话就新招一个核心员工来做 
    • 需要获取全局锁
  2. 核心员工已经最多了,HR 不给批 HC 了,那这个需求只好攒着,放到待完成任务列表吧
  3. 如果列表已经堆满了,核心员工基本没机会搞完这么多任务了,那就找个外包吧 
    • 需要获取全局锁
  4. 如果核心员工 + 外包员工的数量已经是团队最多能承受人数了,没办法,这个需求接不了了

结合这张图,这回流程你明白了吗?

发神经系列-1:线程池_第5张图片

由于 1 和 3 新建线程时需要获取全局锁,这将严重影响性能。因此 ThreadPoolExecutor 这样的处理流程是为了在执行 execute() 方法时尽量少地执行 1 和 3,多执行 2。

在 ThreadPoolExecutor 完成预热后(当前线程数不少于核心线程数),几乎所有的 execute() 都是在执行步骤 2。

前面提到的 ThreadPoolExecutor 构造函数的参数,分别影响以下内容:

  • corePoolSize:核心线程池数量 
    • 在线程数少于核心数量时,有新任务进来就新建一个线程,即使有的线程没事干
    • 等超出核心数量后,就不会新建线程了,空闲的线程就得去任务队列里取任务执行了
  • maximumPoolSize:最大线程数量 
    • 包括核心线程池数量 + 核心以外的数量
    • 如果任务队列满了,并且池中线程数小于最大线程数且大于核心线程池数量,会再创建新的线程执行任务
  • keepAliveTime:核心池以外的线程存活时间,即没有任务的外包的存活时间 
    • 如果给线程池设置 allowCoreThreadTimeOut(true),则核心线程在空闲时头上也会响起死亡的倒计时
    • 如果任务是多而容易执行的,可以调大这个参数,那样线程就可以在存活的时间里有更大可能接受新任务
  • workQueue:保存待执行任务的阻塞队列 
    • 不同的任务类型有不同的选择,下一小节介绍
  • threadFactory:每个线程创建的地方 
    • 可以给线程起个好听的名字,设置个优先级啥的
  • handler:饱和策略,大家都很忙,咋办呢,有四种策略 
    • CallerRunsPolicy:只要线程池没关闭,就直接用调用者所在线程来运行任务
    • AbortPolicy:直接抛出 RejectedExecutionException 异常
    • DiscardPolicy:悄悄把任务放生,不做了
    • DiscardOldestPolicy:把队列里待最久的那个任务扔了,然后再调用 execute() 试试看能行不
    • 我们也可以实现自己的 RejectedExecutionHandler 接口自定义策略,比如如记录日志什么的

保存待执行任务的阻塞队列

当线程池中的核心线程数已满时,任务就要保存到队列中了。

线程池中使用的队列是 BlockingQueue 接口,常用的实现有如下几种:

  • ArrayBlockingQueue:  有界队列,基于数组、有界,按 FIFO(先进先出)原则对元素进行排序
  • LinkedBlockingQueue:界队列基于链表,按FIFO (先进先出) 排序元素 
    • 吞吐量通常要高于 ArrayBlockingQueue
    • Executors.newFixedThreadPool() 使用了这个队列
  • SynchronousQueue:直接提交,不存储元素的阻塞队列 
    • 每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态
    • 吞吐量通常要高于 LinkedBlockingQueue
    • Executors.newCachedThreadPool使用了这个队列
  • PriorityBlockingQueue:具有优先级的、无限阻塞队列

创建自己的线程池

了解上面的内容后,我们就可以创建自己的线程池了。

①先定义线程池的几个关键属性的值:

private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2; // 核心线程数为 CPU 数*2
private static final int MAXIMUM_POOL_SIZE = 64;    // 线程池最大线程数
private static final int KEEP_ALIVE_TIME = 1;    // 保持存活时间 1秒
  • 设置核心池的数量为 CPU 数的两倍,一般是 4、8,好点的 16 个线程
  • 最大线程数设置为 64
  • 空闲线程的存活时间设置为 1 秒

②然后根据处理的任务类型选择不同的阻塞队列

如果是要求高吞吐量的,可以使用 SynchronousQueue 队列;如果对执行顺序有要求,可以使用 PriorityBlockingQueue;如果最大积攒的待做任务有上限,可以使用 LinkedBlockingQueue

private final BlockingQueue mWorkQueue = new LinkedBlockingQueue<>(128);

③然后创建自己的 ThreadFactory

在其中为每个线程设置个名称:

private final ThreadFactory DEFAULT_THREAD_FACTORY = new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);

    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, TAG + " #" + mCount.getAndIncrement());
        thread.setPriority(Thread.NORM_PRIORITY);
        return thread;
    }
};

④然后就可以创建线程池了

private ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME,
        TimeUnit.SECONDS, mWorkQueue, DEFAULT_THREAD_FACTORY,
        new ThreadPoolExecutor.DiscardOldestPolicy());

这里我们选择的饱和策略为 DiscardOldestPolicy,你可以可以创建自己的。

⑤完整代码:

public class ThreadPoolManager {
    private final String TAG = this.getClass().getSimpleName();
    private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2; // 核心线程数为 CPU数*2
    private static final int MAXIMUM_POOL_SIZE = 64;    // 线程队列最大线程数
    private static final int KEEP_ALIVE_TIME = 1;    // 保持存活时间 1秒

    private final BlockingQueue mWorkQueue = new LinkedBlockingQueue<>(128);

    private final ThreadFactory DEFAULT_THREAD_FACTORY = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r, TAG + " #" + mCount.getAndIncrement());
            thread.setPriority(Thread.NORM_PRIORITY);
            return thread;
        }
    };

    private ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME,
            TimeUnit.SECONDS, mWorkQueue, DEFAULT_THREAD_FACTORY,
            new ThreadPoolExecutor.DiscardOldestPolicy());

    private static volatile ThreadPoolManager mInstance = new ThreadPoolManager();

    public static ThreadPoolManager getInstance() {
        return mInstance;
    }

    public void addTask(Runnable runnable) {
        mExecutor.execute(runnable);
    }

    @Deprecated
    public void shutdownNow() {
        mExecutor.shutdownNow();
    }
}

这样我们就有了自己的线程池。

JDK 提供的线程池及使用场景

JDK 为我们内置了五种常见线程池的实现,均可以使用 Executors 工厂类创建。

1.newFixedThreadPool(固定大小的线程池

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

不招外包,有固定数量核心成员的正常互联网团队。

可以看到,FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。

此外 keepAliveTime 为 0,也就是多余的空余线程会被立即终止(由于这里没有多余线程,这个参数也没什么意义了)。

而这里选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,相当于没有上限。

因此这个线程池执行任务的流程如下:

  1. 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务
  2. 线程数等于核心线程数后,将任务加入阻塞队列 
    • 由于队列容量非常大,可以一直加加加
  3. 执行完任务的线程反复去队列中取任务执行

FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。

2.newSingleThreadExecutor(单线程线程池

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

不招外包,只有一个核心成员的创业团队。

从参数可以看出来,SingleThreadExecutor 相当于特殊的 FixedThreadPool,它的执行流程如下:

  1. 线程池中没有线程时,新建一个线程执行任务
  2. 有一个线程以后,将任务加入阻塞队列,不停加加加
  3. 唯一的这一个线程不停地去队列里取任务执行

听起来很可怜的样子 - -。

SingleThreadExecutor 用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。

3.newCachedThreadPool(可缓存线程的线程池

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

全部外包,没活最多待 60 秒的外包团队。

可以看到,CachedThreadPool 没有核心线程,非核心线程数无上限,也就是全部使用外包,但是每个外包空闲的时间只有 60 秒,超过后就会被回收。

CachedThreadPool 使用的队列是 SynchronousQueue,这个队列的作用就是传递任务,并不会保存。

因此当提交任务的速度大于处理任务的速度时,每次提交一个任务,就会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。

它的执行流程如下:

  1. 没有核心线程,直接向 SynchronousQueue 中提交任务
  2. 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
  3. 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就拜拜

由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。

4.newScheduledThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

定期维护的 2B 业务团队,核心与外包成员都有。

ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor, 最多线程数为 Integer.MAX_VALUE ,使用 DelayedWorkQueue 作为任务队列。

ScheduledThreadPoolExecutor 添加任务和执行任务的机制与ThreadPoolExecutor 有所不同。

ScheduledThreadPoolExecutor 添加任务提供了另外两个方法:

  • scheduleAtFixedRate() :按某种速率周期执行
  • scheduleWithFixedDelay():在某个延迟后执行

它俩的代码如下:

public ScheduledFuture scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0L)
        throw new IllegalArgumentException();
    ScheduledFutureTask sft =
        new ScheduledFutureTask(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      unit.toNanos(period),
                                      sequencer.getAndIncrement());
    RunnableScheduledFuture t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}
public ScheduledFuture scheduleWithFixedDelay(Runnable command,
                                                 long initialDelay,
                                                 long delay,
                                                 TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (delay <= 0L)
        throw new IllegalArgumentException();
    ScheduledFutureTask sft =
        new ScheduledFutureTask(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      -unit.toNanos(delay),
                                      sequencer.getAndIncrement());
    RunnableScheduledFuture t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}

可以看到,这两种方法都是创建了一个 ScheduledFutureTask 对象,调用 decorateTask() 方法转成 RunnableScheduledFuture 对象,然后添加到队列中。

看下 ScheduledFutureTask 的主要属性:

private class ScheduledFutureTask<V>
        extends FutureTask<V> implements RunnableScheduledFuture<V> {

    //添加到队列中的顺序
    private final long sequenceNumber;
    //何时执行这个任务
    private volatile long time;
    //执行的间隔周期
    private final long period;
    //实际被添加到队列中的 task
    RunnableScheduledFuture outerTask = this;
    //在 delay queue 中的索引,便于取消时快速查找
    int heapIndex;
    //...
}

DelayQueue 中封装了一个优先级队列,这个队列会对队列中的 ScheduledFutureTask 进行排序,两个任务的执行 time 不同时,time 小的先执行;否则比较添加到队列中的顺序 sequenceNumber ,先提交的先执行。

ScheduledThreadPoolExecutor 的执行流程如下:

  1. 调用上面两个方法添加一个任务
  2. 线程池中的线程从 DelayQueue 中取任务
  3. 然后执行任务

具体执行任务的步骤也比较复杂:

  1. 线程从 DelayQueue 中获取 time 大于等于当前时间的 ScheduledFutureTask 
    • DelayQueue.take()
  2. 执行完后修改这个 task 的 time 为下次被执行的时间
  3. 然后再把这个 task 放回队列中 
    • DelayQueue.add()

ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。

两种提交任务的方法

ExecutorService 提供了两种提交任务的方法:

  1. execute():提交不需要返回值的任务
  2. submit():提交需要返回值的任务

execute

void execute(Runnable command);

execute() 的参数是一个 Runnable,也没有返回值。因此提交后无法判断该任务是否被线程池执行成功。

ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(new Runnable() {
    @Override
    public void run() {
        //do something
    }
});

submit

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future> submit(Runnable task);

submit() 有三种重载,参数可以是 Callable 也可以是 Runnable

  1. 两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
  2. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
  3. Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

同时它会返回一个 Funture 对象,通过它我们可以判断任务是否执行成功。

获得执行结果调用 Future.get() 方法,这个方法会阻塞当前线程直到任务完成。

提交一个 Callable 任务时,需要使用 FutureTask 包一层:

FutureTask futureTask = new FutureTask(new Callable() {    //创建 Callable 任务
    @Override
    public String call() throws Exception {
        String result = "";
        //do something
        return result;
    }
});
Future submit = executor.submit(futureTask);    //提交到线程池
try {
    Object result = submit.get();    //获取结果
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

关闭线程池

线程池即使不执行任务也会占用一些资源,所以在我们要退出任务时最好关闭线程池。

有两个方法关闭线程池:

1.`shutdown()

     public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();  //获取权限
            advanceRunState(SHUTDOWN);  //修改运行状态
            interruptIdleWorkers();  //遍历停止未开启的线程
            onShutdown(); // 目前空实现
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
    private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {  //遍历所有线程
                Thread t = w.thread;
                //多了一个条件w.tryLock(),表示拿到锁后就中断
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

将线程池的状态设置为 SHUTDOWN,然后中断所有 没有执行 的线程,无法再添加线程。

shutdown 方法的作用是向线程池发送关闭的指令。一旦在线程池上调用 shutdown 方法之后,线程池便不能再接受新的任务;如果此时还向线程池提交任务,那么将会抛出 RejectedExecutionException 异常。之后线程池不会立刻关闭,直到之前已经提交到线程池中的所有任务(包括正在运行的任务和在队列中等待的任务)都已经处理完成,才会关闭。

2.shutdownNow()

    public List shutdownNow() {
        List tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);  //修改运行状态
            interruptWorkers();     //中断所有
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

    private void interruptWorkers() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers)  //中断全部线程,不管是否在执行
                w.interruptIfStarted();
        } finally {
            mainLock.unlock();
        }
    }

将线程池设置为 STOP,然后尝试停止 所有线程,并返回等待执行任务的列表。

它们的不同点是:shutdown() 只结束未执行的任务;shutdownNow() 结束全部。

与 shutdown 不同,shutdownNow 会立即关闭线程池 —— 当前在线程池中运行的任务会全部被取消,然后返回线程池中所有正在等待的任务。

共同点是:都是通过遍历线程池中的线程,逐个调用 Thread.interrup() 来中断线程,所以一些无法响应中断的任务可能永远无法停止(比如 Runnable)。

如何合理地选择或者配置

了解 JDK 提供的几种线程池实现,在实际开发中如何选择呢?

根据任务类型决定。 
前面已经介绍了,这里再小节一下:

  • CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。
  • FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。
  • SingleThreadExecutor 用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。
  • ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。

自定义线程池时,如果任务是 CPU 密集型(需要进行大量计算、处理),则应该配置尽量少的线程,比如 CPU 个数 + 1,这样可以避免出现每个线程都需要使用很长时间但是有太多线程争抢资源的情况; 
如果任务是 IO密集型(主要时间都在 I/O,CPU 空闲时间比较多),则应该配置多一些线程,比如 CPU 数的两倍,这样可以更高地压榨 CPU。

为了错误避免创建过多线程导致系统奔溃,建议使用有界队列。因为它在无法添加更多任务时会拒绝任务,这样可以提前预警,避免影响整个系统。

执行时间、顺序有要求的话可以选择优先级队列,同时也要保证低优先级的任务有机会被执行。

总结

借鉴大佬的文章,仅供参考。

Thanks

https://blog.csdn.net/u011240877/article/details/73440993

https://segmentfault.com/a/1190000007925310

https://blog.csdn.net/wolf909867753/article/details/77500625

https://www.cnblogs.com/frinder6/p/5507082.html

http://www.cnblogs.com/absfree/p/5357118.html

你可能感兴趣的:(发神经系列-1:线程池)