多线程(三)线程池

本系列文章:
  多线程(一)线程与进程、Thread
  多线程(二)Java内存模型、同步关键字
  多线程(三)线程池
  多线程(四)显式锁、队列同步器
  多线程(五)可重入锁、读写锁
  多线程(六)线程间通信机制
  多线程(七)原子操作、阻塞队列
  多线程(八)并发容器
  多线程(九)并发工具类
  多线程(十)多线程编程示例

前言 为什么要用线程池*

  线程的开销主要包括以下几个方面:

  • 1、线程的创建和启动的开销
      与普通的对象相比,Java线程还占用了额外的存储空间-----栈空间。并且,线程的启动会产生相应的线程调度开销。
  • 2、线程的销毁
  • 3、线程调度的开销
      线程的调度会导致上下文切换,从而增加处理器资源的消耗,使得应用程序本身可以使用的处理器资源减少。

  一个系统能够创建的线程,受限于该系统所拥有的处理器数目。无论是CPU密集型还是I/O密集型线程,这些线程的数量的临界值总是处理器的数目。

  线程池图示:

  线程池内部可以预先创建一定数量的工作者线程,客户端代码并不需要向线程池借用线程,而是将其执行的任务作为一个对象提交给线程池,线程池可能将这些任务缓存在工作队列之中,而线程池内部的各个工作者线程则不断地从队列中取出任务并执行。因此,线程池可以被看作基于生产者—消费者模式的一种服务,该服务内部维护的工作者线程相当于消费者线程,线程池的客户端相当于生产者线程,客户端代码提交给线程池的任务相当于“产品”,线程池内部用于缓存任务的队列相当于传输通道。
  线程池的主要特点为: 线程复用; 控制最大并发数; 管理线程。
  使用线程池管理线程主要有如下好处:

  • 1、降低资源消耗
      通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗;
  • 2、提升系统响应速度
      通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度;当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 3、提高线程的可管理性
      线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此,需要使用线程池来管理线程。

一、线程池的创建

1.1 线程池相关类

  • 1、Executor
      线程池的顶级接口是Executor,Executor中只定义了一个线程执行的方法:
	public interface Executor {
	    void execute(Runnable command);
	}
  • 2、ExecutorService
      ExecutorService继承了Executor,扩展了线程池的接口:
	public interface ExecutorService extends Executor {
		//关闭线程池,线程池中不再接受新提交的任务,但是之前提交的任务继续运行,直到完成
	    void shutdown();
	    //停止正在执行和待执行的任务,返回待执行任务列表
	    List<Runnable> shutdownNow();
		//如果ExecutorService已关闭,则返回true。
	    boolean isShutdown();
		//判断线程池中的所有任务是否结束,只有在调用shutdown或shutdownNow方法
		//之后调用此方法才会返回true
	    boolean isTerminated();
		//等待线程池中的所有任务执行结束,并设置超时时间
	    boolean awaitTermination(long timeout, TimeUnit unit)
	        throws InterruptedException;
		//提交一个返回值的任务进行执行,并返回一个代表任务未决结果的Future。
		//Future的get方法将在任务成功完成后返回任务的结果。
	    <T> Future<T> submit(Callable<T> task);
		//提交Runnable任务以供执行并返回代表该任务的Future。 
		//Future的get方法将在任务成功完成后返回给定的result。
	    <T> Future<T> submit(Runnable task, T result);
		//提交Runnable任务以供执行并返回代表该任务的Future。Future的get方法
		//将在成功完成后返回null。
	    Future<?> submit(Runnable task);
		//批量提交任务并获得他们的future,Task列表与Future列表一一对应
	    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
	        throws InterruptedException;
		//批量提交任务并获得他们的future,并限定处理所有任务的时间
	    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
	                                  long timeout, TimeUnit unit)
	        throws InterruptedException;
		//批量提交任务并获得一个已经成功执行的任务的结果
	    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
	        throws InterruptedException, ExecutionException;
		//批量提交任务并获得一个已经成功执行的任务的结果,并限定处理任务的时间
	    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
	                    long timeout, TimeUnit unit)
	        throws InterruptedException, ExecutionException, TimeoutException;
	}
  • 3、ThreadPoolExecutor
      ThreadPoolExecutor继承AbstractExecutorService,AbstractExecutorService实现ExecutorService接口。
      ThreadPoolExecutor是用来创建线程池的主要类,Executors工具类创建线程池本质上也是通过ThreadPoolExecutor实现
  • 4、ScheduledExecutorService
      ScheduledExecutorService接口派生自ExecutorService接口,继承了ExecutorService接口的所有功能,并提供了定时处理任务的能力。
	public interface ScheduledExecutorService extends ExecutorService {
		//带延迟时间的调度,只执行一次
		//调度之后可通过Future.get()阻塞直至任务执行完毕
	    public ScheduledFuture<?> schedule(Runnable command,
	                                       long delay, TimeUnit unit);
	                                       
		//带延迟时间的调度,只执行一次
 		//调度之后可通过Future.get()阻塞直至任务执行完毕,并且可以获取执行结果
	    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
	                                           long delay, TimeUnit unit);
	                                           
		//带延迟时间的调度,循环执行,固定频率
	    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
	                                                  long initialDelay,
	                                                  long period,
	                                                  TimeUnit unit);
		//带延迟时间的调度,循环执行,固定延迟
	    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
	                                                     long initialDelay,
	                                                     long delay,
	                                                     TimeUnit unit);
	}
  • 5、ScheduledThreadPoolExecutor
      ScheduledThreadPoolExecutor继承ThreadPoolExecutor,实现ScheduledExecutorService接口,是用来执行周期性任务调度的线程池。
  • 6、Executors
      线程池生成工具类,在Executors类里面提供了一些静态工厂,生成一些常用的线程池。
      Executors工厂类可以创建四种线程池,分别为:
  1. newCachedThreadPool :创建一个可缓存线程池。如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,否则新建线程。(线程最大并发数不可控制)
  2. newFixedThreadPool:创建一个固定大小的线程池。可控制线程最大并发数,超出的线程会在队列中等待。
  3. newScheduledThreadPool : 创建一个定时线程池,支持定时及周期性任务执行。
  4. newSingleThreadExecutor :创建一个单线程化的线程池。它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

1.2 使用ThreadPoolExecutor创建线程池(建议使用)*

  通过ThreadPoolExecutor的方式创建线程池,可以让开发者更加明确线程池的运行规则,规避资源耗尽的风险。
  创建线程池主要是ThreadPoolExecutor类来完成,ThreadPoolExecutor的有许多构造方法,可以通过参数最多的构造方法来理解创建线程池有哪些需要配置的参数。参数最多(7个)的构造方法为:

	ThreadPoolExecutor(int corePoolSize,
	                   int maximumPoolSize,
	                   long keepAliveTime,
	                   TimeUnit unit,
	                   BlockingQueue<Runnable> workQueue,
	                   ThreadFactory threadFactory,
	                   RejectedExecutionHandler handler)

  ThreadPoolExecutor执行execute方法图示:

1.2.1 corePoolSize*

  核心线程池的大小,即在没有任务需要执行的时候线程池的大小
  需要注意的是:在刚刚创建ThreadPoolExecutor的时候,线程并不会立即启动,而是要等到有任务提交时才会启动,除非调用了prestartCoreThread或prestartAllCoreThreads事先启动核心线程。
  当有新任务时,如果线程池中线程数没有达到线程池的基本大小,则会创建新的线程执行任务,否则将任务放入阻塞队列。

  当线程池中存活的线程数总是大于corePoolSize时,应该考虑调大corePoolSize。

1.2.2 maximumPoolSize*

  线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。
  当阻塞队列填满时,如果线程池中线程数没有超过最大线程数,则会创建新的线程运行任务。否则根据拒绝策略处理新任务。非核心线程类似于临时借来的资源,这些线程在空闲时间超过keepAliveTime之后,就会退出,避免资源浪费。

1.2.3 keepAliveTime*

  空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。
  非核心线程空闲后,保持存活的时间,此参数只对非核心线程有效。

  设置为0,表示多余的空闲线程会被立即终止。

1.2.4 unit*

  时间单位,为keepAliveTime指定时间单位,可以指定为:秒,毫秒,微秒,纳秒等,具体为:

    TimeUnit.DAYS
    TimeUnit.HOURS
    TimeUnit.MINUTES
    TimeUnit.SECONDS
    TimeUnit.MILLISECONDS
    TimeUnit.MICROSECONDS
    TimeUnit.NANOSECONDS
1.2.5 workQueue*

  保存任务的阻塞队列。当调用execute方法时,如果线程池中没有空闲的可用线程,那么就会把这个Runnable对象放到该队列中。这个参数必须是一个实现BlockingQueue接口的阻塞队列,因为要保证线程安全。
  常见阻塞队列有4种:

  • 1、ArrayBlockingQueue
      一个用数组实现的有界阻塞队列,按照先入先出(FIFO)的原则对元素进行排序。可以设置是否采用公平策略,使用较少。
  • 2、PriorityBlockingQueue
      支持优先级的无界阻塞队列,使用较少。
  • 3、LinkedBlockingQueue
      一个用链表实现的有界阻塞队列,队列默认和最长长度为Integer.MAX_VALUE。队列按照先入先出的原则对元素进行排序,使用较多。吞吐量通常要高于ArrayBlockingQueue
  • 4、SynchronousQueue
      不储存元素(无容量)的阻塞队列,每个put操作必须等待一个take操作,否则不能继续添加元素。支持公平访问队列,常用于生产者,消费者模型,吞吐量较高,使用较多。
1.2.6 threadFactory*

  创建线程的工厂类,一般用默认即可,默认实现是Executors.defaultThreadFactory()。也可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,方便查找问题原因。示例:

public class MyThreadFactory implements ThreadFactory {
    private final String poolName;
    public MyThreadFactory(String poolName) {
        this.poolName = poolName;
    }
 
    public Thread newThread(Runnable runnable) {
        //将线程池名字传递给构造函数,用于区分不同线程池的线程
        return new MyAppThread(runnable, poolName);
    }
}
1.2.7 handler*

  饱和策略。当线程池的阻塞队列已满和指定的线程都已经运行,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。采用的策略有这几种:

  • AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;
  • CallerRunsPolicy:只用调用者所在的线程来执行任务;
  • DiscardPolicy:不处理直接丢弃掉任务;
  • DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务。
  • RejectedExecutionHandler接口
      RejectedExecutionHandler接口用于封装被拒绝的任务的处理策略,该接口中只有一个方法:
	public interface RejectedExecutionHandler {
	    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
	}

  r参数代表被拒绝的任务,e代表拒绝任务r的线程池实例。
  上面说的4种拒绝策略,就对应了ThreadPoolExecutor中提供的4种RejectedExecutionHandler实现类:

RejectedExecutionHandler实现类所实现的策略
ThreadPoolExecutor.AbortPolicy默认的拒绝策略,直接抛出异常
ThreadPoolExecutor.DiscardPolicy丢弃当前被拒绝的任务,不抛出异常
ThreadPoolExecutor.DiscardOldestPolicy将工作队列中最久的任务丢弃,然后重新尝试被拒绝的任务
ThreadPoolExecutor.CallerRunsPolicy将被拒绝的任务返回给调用者处理

  除了4种自定义策略,开发者也可以自定义拒绝策略。比如:比如想让被拒绝的任务在一个新的线程中执行,示例:

static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        new Thread(r,"新线程"+new Random().nextInt(10)).start();
    }
}
1.2.8 corePoolSize、maximumPoolSize和workQueue
  • 1、三个参数的关系
      如果运行的线程数小于corePoolSize,直接创建新线程处理任务,即使线程池中的其他线程是空闲的。
      如果运行的线程数大于等于corePoolSize,并且小于maximumPoolSize,此时,只有当workQueue满时,才会创建新的线程处理任务。
      如果设置的corePoolSize与maximumPoolSize相同,那么创建的线程池大小是固定的。此时,如果有新任务提交,并且workQueue没有满时,就把请求放入到workQueue中,等待空闲的线程,从workQueue中取出任务进行处理。
      如果运行的线程数量大于maximumPoolSize,同时,workQueue已经满了,会通过拒绝策略参数rejectHandler来指定处理策略。
  • 队列对线程池处理方式的影响
      当提交一个新的任务到线程池时,线程池会根据当前线程池中正在运行的线程数量来决定该任务的处理方式。处理方式总共有三种:直接切换、使用无限队列、使用有界队列。
      直接切换常用的队列就是SynchronousQueue。
      使用无限队列就是使用基于链表的队列,比如:LinkedBlockingQueue,如果使用这种方式,线程池中创建的最大线程数就是corePoolSize,此时maximumPoolSize不会起作用。当线程池中所有的核心线程都是运行状态时,提交新任务,就会放入等待队列中。
      使用有界队列使用的是ArrayBlockingQueue,使用这种方式可以将线程池的最大线程数量限制为maximumPoolSize,可以降低资源的消耗。但是,这种方式使得线程池对线程的调度更困难,因为线程池和队列的容量都是有限的了。
  • 降低系统资源消耗的一些措施
      如果想降低系统资源的消耗,包括CPU使用率,操作系统资源的消耗,上下文环境切换的开销等,可以设置一个较大的队列容量和较小的线程池容量。这样,会降低线程处理任务的吞吐量。
      如果提交的任务经常发生阻塞,可以考虑调用设置最大线程数的方法,重新设置线程池最大线程数。如果队列的容量设置的较小,通常需要将线程池的容量设置的大一些,这样,CPU的使用率会高些。如果线程池的容量设置的过大,并发量就会增加,则需要考虑线程调度的问题,反而可能会降低处理任务的吞吐量。

1.3 使用Executors创建线程池(不建议用)

  Executors工厂类可以创建四种线程池:newCachedThreadPool 、newFixedThreadPool、newScheduledThreadPool和newSingleThreadExecutor。

1.3.1 newFixedThreadPool

  FixedThreadPool被称为可重用固定线程数的线程池。newFixedThreadPool的实现:

	/**
	 * 核心线程池大小=最大线程池大小=传入参数
	 * 线程过期时间为0ms
	 * LinkedBlockingQueue作为工作队列
	 */
	public static ExecutorService newFixedThreadPool(int nThreads) {
	    return new ThreadPoolExecutor(nThreads, nThreads,
	                                  0L, TimeUnit.MILLISECONDS,
	                                  new LinkedBlockingQueue<Runnable>());
	}

  newFixedThreadPool方法里的构造方法,其实调用了重载ThreadPoolExecutor方法:

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

	private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();

  newFixedThreadPool的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列
  此外keepAliveTime为0,也就是多余的空余线程会被立即终止(由于这里没有多余线程,这个参数也没什么意义)。
  newFixedThreadPool选用的阻塞队列是LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,相当于没有上限,可认为是无界队列。
  newFixedThreadPool适用场景:适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。需要注意的是,FixedThreadPool 不会拒绝任务,在任务比较多的时候会导致OOM。
  newFixedThreadPool线程池执行任务的流程:

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

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

1.3.2 newSingleThreadExecutor

  SingleThreadExecutor是使用单个worker线程的线程池。newSingleThreadExecutor的实现:

/**
 * 核心线程池大小=最大线程池大小=1
 * 线程过期时间为0ms
 * LinkedBlockingQueue作为工作队列
 */
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

  从参数可以看出来,SingleThreadExecutor相当于特殊的FixedThreadPool,这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  SingleThreadExecutor的执行流程:

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

  SingleThreadExecutor适用场景:适用于串行执行任务的场景,一个任务一个任务地执行。在任务比较多的时候也是会导致OOM。
  SingleThreadExecutor用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。

1.3.3 newCachedThreadPool

  CachedThreadPool是一个会根据需要创建新线程的线程池。newCachedThreadPool的实现:

	/**
	 *  核心线程池大小=0
	 *  最大线程池大小为Integer.MAX_VALUE
	 *  线程过期时间为60s
	 *  使用SynchronousQueue作为工作队列
	 */
	public static ExecutorService newCachedThreadPool() {
	    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
	                                  60L, TimeUnit.SECONDS,
	                                  new SynchronousQueue<Runnable>());
	}

  可缓存的线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
  newCachedThreadPool使用的队列是SynchronousQueue,这个队列的作用就是传递任务,并不会保存
  如果主线程提交任务的速度高于线程处理任务的速度时, CachedThreadPool会不断创建新的线程。
  使用没有容量的SynchronousQueue作为线程池工作队列,当线程池有空闲线程时,SynchronousQueue.offer(Runnable task) 提交的任务会被空闲线程处理,否则会创建新的线程处理任务。
  CachedThreadPool适用场景:用于并发执行大量短期的小任务。 CachedThreadPool 允许创建的线程数量为Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

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

  1. 没有核心线程,直接向SynchronousQueue中提交任务;
  2. 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个;
  3. 执行完任务的线程有60秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就拜拜;
  4. 由于空闲60秒的线程会被终止,长时间保持空闲的CachedThreadPool不会占用任何资源。

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

1.3.4 newScheduledThreadPool

  支持定时以及周期性执行任务的需求的线程池。
  newScheduledThreadPool的实现:

	/**
	 * 核心线程池大小=传入参数
	 * 最大线程池大小为Integer.MAX_VALUE
	 * 线程过期时间为0ms
	 * DelayedWorkQueue作为工作队列
	 */
	public ScheduledThreadPoolExecutor(int corePoolSize) {
	    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
	          new DelayedWorkQueue());
	}

  大小无限(实际上有限,为Integer.MAX_VALUE)的线程池。
  ScheduledThreadPoolExecutor 的执行流程:

  1. 添加一个任务;
  2. 线程池中的线程从DelayQueue中取任务;
  3. 然后执行任务。

  具体执行任务的步骤:

  1. 线程从 DelayQueue 中获取已到期的 ScheduledFutureTask(DelayQueue.take()) 。到期任务是指 ScheduledFutureTask 的 time 大于等于当前系统的时间;
  2. 执行这个 ScheduledFutureTask ;
  3. 修改 ScheduledFutureTask 的 time 变量为下次将要被执行的时间;
  4. 把这个修改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中( DelayQueue.add() )。


  在给定的延迟后运行任务,或者定期执行任务。在实际项目中基本不会被用到,因为有其他方案选择比如quartz。
  使用的任务队列 DelayQueue 封装了一个 PriorityQueue , PriorityQueue 会对队列中的任务进行排序,时间早的任务先被执行(即 ScheduledFutureTask 的 time 变量小的先执行),如果time相同则先提交的任务会被先执行( ScheduledFutureTask 的 squenceNumber 变量小的先执行)。

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

1.4 使用Executors和ThreadPoolExecutor创建线程池的区别

  使用Executors创建线程池的缺点:

  • newFixedThreadPool和newSingleThreadExecutor
      newFixedThreadPool和newSingleThreadExecutor使用的队列是LinkedBlockingQueue,相当于无界队列(队列最大值是Integer.MAX_VALUE)。因此堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
  • newCachedThreadPool和newScheduledThreadPool
      newCachedThreadPool和newScheduledThreadPool的最大线程数是Integer.MAX_VALUE。因此可能会创建数量非常多的线程,甚至OOM。

  《阿里巴巴Java开发手册》中强制线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让开发者更加明确线程池的运行规则,规避资源耗尽的风险
  ThreaPoolExecutor创建线程池方式只有一种,就是使用它的构造函数,参数由开发者自己指定。

1.5 线程池参数配置*

  如果线程池线程数量太小,当有大量请求需要处理,系统响应比较慢影响体验,甚至会出现任务队列大量堆积任务导致OOM。
  如果线程池线程数量过大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换(cpu给线程分配时间片,当线程的cpu时间片用完后保存状态,以便下次继续运行),从 而增加线程的执行时间,影响了整体执行效率。
  要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析:

  1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务
  2. 任务的优先级:高,中和低。
  3. 任务的执行时间:长,中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

  任务性质不同的任务可以用不同规模的线程池分开处理。示例:

  • 1、CPU密集型任务
      这种任务消耗的主要是CPU资源,可以将线程数设置为N(CPU核心数)
    +1,比CPU核心数多出来的一个线程是为了防止某些原因导致的任务暂停(线程阻塞,如io操作,等待锁,线程sleep)而带来的影响。一旦某个线程被阻塞,释放了CPU资源,而在这种情况下多出来的一个线程就可以充分利用CPU的空闲时间。
  • 2、IO密集型任务
      系统会用大部分的时间来处理I/O操作,而线程等待I/O操作会被阻塞,释放CPU资源,这时就可以将CPU交出给其它线程使用。因此在I/O密集型任务的应用中,我们可以多配置一些线程,具体的计算方法:最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (I/O耗时/CPU耗时)),一般可设置为2N。

1.6 线程池的监控

1.6.1 获取ThreadPoolExecutor的使用情况

  ThreadPoolExecutor中有一些属性,可以用来查看线程池的使用情况。

    //线程池需要执行的任务数量taskCount
	public long getTaskCount()
    //线程池在运行过程中已完成的任务数量,小于或等于taskCount
	public long getCompletedTaskCount()
    //线程池里曾经创建过的最大线程数量,通过这个数据可以知道线程池是否曾经满过
    //如该数值等于线程池的最大大小,则表示线程池曾经满过
	public int getLargestPoolSize()
    //获得当前线程量(包含核心线程和临时线程)
	public int getPoolSize()
    //获取线程池中活动线程数量
	public int getActiveCount()
1.6.2 重写ThreadPoolExecutor中的方法

  可以通过继承ThreadPoolExecutor来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated等方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。
  ThreadPoolExecutor中的一些空方法:

	protected void beforeExecute(Thread t, Runnable r) { }
	protected void afterExecute(Runnable r, Throwable t) { }
	protected void terminated() { }

1.7 线程池相关问题

1.7.1 线程数过多会造成什么问题
  • 1、线程的生命周期开销非常高
  • 2、消耗过多的CPU资源
      如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。
  • 3、降低稳定性
      JVM在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM的启动参数、Thread构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OOM异常。
1.7.2 Executor、ExecutorService、Executors的区别

  Executors类提供工厂方法用来创建不同类型的线程池。
  Executor和ExecutorService这两个接口主要的区别是:

  1. ExecutorService接口继承了Executor接口,是Executor的子接口。
  2. Executor和ExecutorService第二个区别是:Executor接口定义了execute()方法用来接收一个Runnable接口的对象,而ExecutorService接口中的submit()方法可以接受Runnable和Callable接口的对象。
  3. Executor和ExecutorService接口第三个区别是Executor中的execute()方法不返回任何结果,而ExecutorService中的submit()方法可以通过一个Future对象返回运算结果。
  4. Executor和ExecutorService接口第四个区别是除了允许客户端提交一个任务,ExecutorService还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。
1.7.3 线程池中的所有线程超过了空闲时间都会被销毁么?

  如果allowCoreThreadTimeOut为true,超过了空闲时间的所有线程都会被回收,不过这个值默认是false,系统会保留核心线程,其他的会被回收。

二、线程池的使用

2.1 提交任务

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

  1. execute():提交不需要返回值的任务
  2. submit():提交需要返回值的任务
2.1.1 execute
	void execute(Runnable command);

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

	ExecutorService executor = Executors.newCachedThreadPool();
	executor.execute(new Runnable() {
	    @Override
	    public void run() {
	        //do something
	    }
	});
2.1.2 submit
	/*
	 * Callable接口中只定义了一个方法:
	 * V call() throws Exception
	 * Callable接口相当于一个增强型的Runnable接口,call方法的返回值代表相应任务
	 * 的处理结果,而Runnable接口中的run方法既无返回值也不能抛出异常。
	 */
	<T> Future<T> submit(Callable<T> task);
	<T> Future<T> submit(Runnable task, T result);
	Future<?> submit(Runnable task);

  可以看出:submit()有三种重载,参数可以是Callable,也可以是Runnable
  同时它会返回一个Funture对象,通过该对象可以判断任务是否执行成功。
  如果要获得执行结果,可以调用Future.get() 方法,这个方法会阻塞当前线程直到任务完成。
  提交一个Callable任务时,需要使用FutureTask包一层,示例:

	FutureTask futureTask = new FutureTask(new Callable<String>() {    
		//创建Callable任务
	    @Override
	    public String call() throws Exception {
	        String result = "";
	        //do something
	        return result;
	    }
	});
	//提交到线程池
	Future<?> future = executor.submit(futureTask);  
	//获取结果  
	try {
	    Object result = future.get();    
	} catch (InterruptedException e) {
	    e.printStackTrace();
	} catch (ExecutionException e) {
	    e.printStackTrace();
	}
2.1.3 submit和execute的区别*
submitexecute
接收参数可以执行Runnable和Callable类型的任务只能执行Runnable类型的任务
返回值可以返回持有计算结果的Future对象
异常处理方便异常处理
2.1.4 runnable和callable的区别

  Callable接口是JDK1.5新增的泛型接口,在JDK1.8中,被声明为函数式接口:

	@FunctionalInterface
	public interface Callable<V> {
  		V call() throws Exception;
	}

  Callable类似于Runnable,两者都可以执行任务。
  Future接口表示异步任务,是一个可能还没有完成的异步任务的结果。Callable用于产生结果,Future 用于获取结果

  • runnable和callable的相同点:
  1. 都是接口;
  2. 都可以编写多线程程序;
  3. 都采用Thread.start()启动线程。
  • runnable和callable的主要区别:
  1. Runnable接口中的run方法无返回值;Callable接口中的call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  2. Runnable接口中的run方法只能抛出运行时异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息

  Callalbe接口支持返回执行结果,需要调用FutureTask.get(),此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞
  工具类Executors可以实现Runnable对象和Callable对象之间的相互转换。( Executors.callable(Runnable task) 或 Executors.callable(Runnable task,Object resule) )。

2.2 获取线程执行结果

  在向线程池中提交任务时用到了Future。Future接口表示代表异步计算的结果,FutureTask是Future常见的实现类。

  • 同步任务
      以同步方式执行的任务,任务的发起和任务的执行是串行的。同步任务就像打电话:先拨打对方的号码(任务的发起),只有在电话接通(任务开始执行)之后,才能将消息告诉对方(任务执行的过程)。
  • 异步任务
      以异步方式执行的任务,任务的发起和任务的执行是并发的。异步任务就像发短信:只要给对方发一条短信(任务的发起),便认为通知到对方了,而不必关心对方什么时候看这条短信(任务)开始执行。
2.2.1 Future

  Future是JDK1.5新增的异步编程接口。
  Future是异步计算结果的顶级接口,定义了一些用于异步计算的方法:

public interface Future<V> {
	//取消任务的执行,接收一个boolean类型的参数,成功取消任务,则返回true,否则返回false。
	//当任务已经完成,已经结束或者因其他原因不能取消时,方法会返回false,表示任务取消失败。
	//当任务未启动调用了此方法,并且结果返回true(取消成功),则当前任务不再运行。如果任务
	//已经启动,会根据当前传递的boolean类型的参数来决定是否中断当前运行的线程来取消当前运行的任务。
    boolean cancel(boolean mayInterruptIfRunning);
	//判断任务在完成之前是否被取消,如果在任务完成之前被取消,则返回true;否则,返回false。
	//需要注意:只有任务未启动,或者在完成之前被取消,才会返回true,表示任务已经被成功取消。
	//其他情况都会返回false。
    boolean isCancelled();
    //判断任务是否已经完成,如果任务正常结束、抛出异常退出、被取消,都会返回true,表示
    //任务已经完成。
    boolean isDone();
    //当任务完成时,直接返回任务的结果数据;当任务未完成时,等待任务完成并返回任务的结果数据。
    V get() throws InterruptedException, ExecutionException;
	//当任务完成时,直接返回任务的结果数据;当任务未完成时,等待任务完成,并设置了超时等待时间。
	//在超时时间内任务完成,则返回结果;否则,抛出TimeoutException异常。
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
2.2.2 FutureTask的状态

  FutureTask 表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,通过FutureTask可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成,调用get方法将会阻塞。
  一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装。
  由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
  FutureTask也可以由调用线程直接执行(FutureTask.run())。根据FutureTask.run()方法被执行的时机,FutureTask可以处于下面3种状态:

  • 1、未启动
      FutureTask.run()方法还没有被执行之前,FutureTask处于未启动状态。当创建一个FutureTask,且没有执行FutureTask.run()方法之前,这个FutureTask处于未启动状态。
  • 2、已启动
      FutureTask.run()方法被执行的过程中,FutureTask处于已启动状态。
  • 3、已完成
      FutureTask.run()方法执行完后正常结束,或被取消,或执行FutureTask.run()方法时抛出异常而异常结束,FutureTask处于已完成状态。

  FutureTask状态装换图示:

  FutureTask的状态有以上三种,在不同状态时结果也不同。

  • 执行get方法时,可能的结果:
  1. 当FutureTask处于未启动或已启动状态时,执行FutureTask.get()方法将导致调用线程阻塞;
  2. 当FutureTask处于已完成状态时,执行FutureTask.get()方法将导致调用线程立即返回结果或抛出异常;
  • 执行cancel方法时,可能的结果:
  1. 当FutureTask处于未启动状态时,执行FutureTask.cancel()方法将导致此任务永远不会被执行;
  2. 当FutureTask处于已启动状态时,执行FutureTask.cancel(true)方法将以中断执行此任务线程的方式来试图停止任务;
  3. 当FutureTask处于已启动状态时,执行FutureTask.cancel(false)方法将不会对正在执行此任务的线程产生影响(让正在执行的任务运行完成);
  4. 当FutureTask处于已完成状态时,执行FutureTask.cancel(…)方法将返回false。

  FutureTask的get和cancel的执行图示:

2.2.3 FutureTask的使用

  FutureTask可用于异步获取执行结果或取消执行任务的场景。
  可以把FutureTask交给Executor执行;也可以通过ExecutorService.submit(…)方法返回一个FutureTask,然后执行FutureTask.get()方法或FutureTask.cancel(…)方法。除此以外,还可以单独使用FutureTask。
  通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果,因此,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
  利用FutureTask和ExecutorService,可以用多线程的方式提交计算任务,主线程继续执行其他任务。示例:

public class FutureTaskTest1 {
	
	public static void main(String[] args) {
		FutureTaskTest1 futureTaskTest1 = new FutureTaskTest1();
	    //创建任务集合
	    List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>();
	    ExecutorService threadPool = Executors.newFixedThreadPool(5);
	    for (int i = 0; i < 10; i++) {
	        //传入Callable对象创建FutureTask对象
	        FutureTask<Integer> ft = new FutureTask<Integer>(futureTaskTest1.new ComputeTask(i, ""+i));
	        taskList.add(ft);
	        //提交给线程池执行任务,也可以通过exec.invokeAll(taskList)一次性提交所有任务;
	        threadPool.submit(ft);
	     }

	     System.out.println("所有计算任务提交完毕, 主线程暂时不关心计算结果");

	     //开始统计各计算线程计算结果
	     Integer totalResult = 0;
	     for(FutureTask<Integer> ft : taskList){
	         try {
	             //FutureTask的get方法会自动阻塞,直到获取计算结果为止
	             totalResult = totalResult + ft.get();
	         } catch (InterruptedException e) {
	             e.printStackTrace();
	         } catch (ExecutionException e) {
	             e.printStackTrace();
	         }
	     }
	     
	     //关闭线程池
	     threadPool.shutdown();
	     System.out.println("多任务计算后的总结果是:" + totalResult);
	}

	private class ComputeTask implements Callable<Integer> {
		private Integer result = 0;
	    private String taskName = "";

	    public ComputeTask(Integer iniResult, String taskName){
	        result = iniResult;
	        this.taskName = taskName;
	        System.out.println("生成子线程计算任务: "+taskName);
	    }

	    @Override
	    public Integer call() throws Exception {
	        for (int i = 0; i < 100; i++) {
	            result =+ i;
	        }
	        //休眠5秒钟,观察主线程行为,预期的结果是主线程会继续执行,到要取得FutureTask的结果是等待直至完成。
	        Thread.sleep(5000);
	        System.out.println("子线程计算任务: "+taskName+" 执行完成!");
	        return result;
	    }
	}
}

  结果示例:

生成子线程计算任务: 0
生成子线程计算任务: 1
生成子线程计算任务: 2
生成子线程计算任务: 3
生成子线程计算任务: 4
生成子线程计算任务: 5
生成子线程计算任务: 6
生成子线程计算任务: 7
生成子线程计算任务: 8
生成子线程计算任务: 9
所有计算任务提交完毕, 主线程暂时不关心计算结果
子线程计算任务: 1 执行完成!
子线程计算任务: 0 执行完成!
子线程计算任务: 3 执行完成!
子线程计算任务: 4 执行完成!
子线程计算任务: 2 执行完成!
子线程计算任务: 5 执行完成!
子线程计算任务: 6 执行完成!
子线程计算任务: 7 执行完成!
子线程计算任务: 8 执行完成!
子线程计算任务: 9 执行完成!
多任务计算后的总结果是:990

2.2.4 FutureTask的实现

  FutureTask的实现基于AbstractQueuedSynchronizer(AQS)。JUC中的很多可阻塞类(比如ReentrantLock)都是基于AQS来实现的。AQS是一个同步框架,它提供通用机制来原子性管理同步状态、阻塞和唤醒线程,以及维护被阻塞线程的队列。基于AQS实现的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch和FutureTask。
  每一个基于AQS实现的同步器都会包含两种类型的操作:

  • 1、至少一个acquire操作
      这个操作阻塞调用线程,除非/直到AQS的状态允许这个线程继续执行。FutureTask的acquire操作为get()/get(long timeout,TimeUnit unit)方法调用。
  • 2、至少一个release操作
      这个操作改变AQS的状态,改变后的状态可允许一个或多个阻塞线程被解除阻塞。FutureTask的release操作包括run()方法和cancel(…)方法。

  基于“复合优先于继承”的原则,FutureTask声明了一个内部私有的继承于AQS的子类Sync,对FutureTask所有公有方法的调用都会委托给这个内部子类。
  AQS被作为“模板方法模式”的基础类提供给FutureTask的内部子类Sync,这个内部子类只需要实现状态检查和状态更新的方法即可,这些方法将控制FutureTask的获取和释放操作。具体来说,Sync实现了AQS的tryAcquireShared(int)方法和tryReleaseShared(int)方法,Sync通过这两个方法来检查和更新同步状态。
  FutureTask的设计:

  Sync是FutureTask的内部私有类,它继承自AQS。创建FutureTask时会创建内部
私有的成员对象Sync,FutureTask所有的的公有方法都直接委托给了内部私有的Sync。

  • FutureTask.get(实际调用调用AQS.acquireSharedInterruptibly(int arg))的执行过程
      1)调用AQS.acquireSharedInterruptibly(int arg)方法,这个方法首先会回调在子类Sync中实现的tryAcquireShared()方法来判断acquire操作是否可以成功。acquire操作可以成功的条件为:state为执行完成状态RAN或已取消状态CANCELLED,且runner不为null。
      2)如果成功则get()方法立即返回。如果失败则到线程等待队列中去等待其他线程执行release操作。
      3)当其他线程执行release操作(比如FutureTask.run()或FutureTask.cancel(…))唤醒当前线程后,当前线程再次执行tryAcquireShared()将返回正值1,当前线程将离开线程等待队列并唤醒它的后继线程。
      4)最后返回计算的结果或抛出异常。
  • FutureTask.run的执行过程
      1)执行在构造函数中指定的任务(Callable.call())。
      2)以原子方式来更新同步状态(调用AQS.compareAndSetState(int expect,int update),设置state为执行完成状态RAN)。如果这个原子操作成功,就设置代表计算结果的变量result的值为Callable.call()的返回值,然后调用AQS.releaseShared(int arg)。
      3)AQS.releaseShared(int arg)首先会回调在子类Sync中实现的tryReleaseShared(arg)来执行release操作(设置运行任务的线程runner为null,然会返回true);AQS.releaseShared(int arg),然后唤醒线程等待队列中的第一个线程。
      4)调用FutureTask.done()。

2.3 线程池的关闭

  要关闭线程池,有shutdown和shutdownNow两个方法:

	public void shutdown()
	public List<Runnable> shutdownNow()
  • 1、shutdown()
      把线程池的状态设置成SHUTDOWN状态,然后中断所有没有正执行任务的线程。
  • 2、shutdownNow()
      首先把线程池的状态设置成STOP,然后尝试停止所有正在执行任务或者暂停任务的线程,并返回等待执行任务的列表。

  要检测线程池中的状态,有以下几个方法:

  • 1、isShutdown()
      只要调用了shutdown或者shutdownNow,isShutdown就会返回true,否则false。
  • 2、isTerminaed()
      当所有任务关闭之后,才表示线程池关闭成功,这时会返回true。
  • 3、isTerminating()
      执行了shutdown或shutdownNow之后,还有任务正在进行中的话,则返回值为true;没有任务进行中的话,返回值为false。

三、ScheduledThreadPoolExecutor

  ScheduledThreadPoolExecutor是个特殊的线程池,其可以用来在给定延时后执行异步任务或者周期性执行任务。
  @Scheduled注解的底层实现也是ScheduledThreadPoolExecutor。

  在SpringBoot的自动化配置中,会给我们自动初始化一个 核心线程为 1,无界阻塞队列的ScheduledThreadPoolExecutor线程池,所以所有的定时任务都是同步阻塞串行运行的。

  @Scheduled定时任务是同步阻塞任务,因为它所使用的线程池是一个单线程的线程池,这意味着所有任务都是串行执行,只要前一个任务未执行完成,后面的任务都会一直等待下去,并且当一个任务未执行完成,它的下个触发周期会被忽略。
  因为这些特点,当我们项目中的定时任务比较密集并且耗时比较长的时候需要特别注意,必要时需要我们提供自己配置的线程池。

ScheduledThreadPoolExecutor类的继承关系:

  从继承关系看ScheduledThreadPoolExecutor的特点:

  1. 可以通过execute()和submit()提交异步任务;
  2. 能够延时执行任务和周期执行任务。

3.1 ScheduledThreadPoolExecutor的创建

3.1.1 构造方法
	public ScheduledThreadPoolExecutor(int corePoolSize) {
	    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
	          new DelayedWorkQueue());
	}
	
	public ScheduledThreadPoolExecutor(int corePoolSize,
	                                   ThreadFactory threadFactory) {
	    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
	          new DelayedWorkQueue(), threadFactory);
	}
	
	public ScheduledThreadPoolExecutor(int corePoolSize,
	                                   RejectedExecutionHandler handler) {
	    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
	          new DelayedWorkQueue(), handler);
	}
	
	public ScheduledThreadPoolExecutor(int corePoolSize,
	                                   ThreadFactory threadFactory,
	                                   RejectedExecutionHandler handler) {
	    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
	          new DelayedWorkQueue(), threadFactory, handler);
	}

  由于ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,它的构造方法实际上是调用了ThreadPoolExecutor的构造方法。
  在上面的构造方法中,用到的队列是DelayQueue。DelayQueue是一个无界队列,所以maximumPoolSize在ScheduledThreadPoolExecutor中没有什么意义(设置maximumPoolSize的大小没有什么效果)。

3.1.2 DelayedWorkQueue

  定时任务执行时需要取出最近要执行的任务,所以任务在队列中每次出队时一定要是当前队列中执行时间最靠前的,所以自然要使用优先级队列。DelayedWorkQueue是一个(基于堆的)优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的,由于它是基于堆结构的队列,堆结构在执行插入和删除操作时的最坏时间复杂度是 O(logN)。
  DelayedWorkQueue的部分源码:

	//初始大小
	private static final int INITIAL_CAPACITY = 16;
	//DelayedWorkQueue是由一个大小为16的数组组成,数组元素为实现RunnableScheduleFuture接口的类
	//实际上为ScheduledFutureTask
	private RunnableScheduledFuture<?>[] queue =
	    new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
	private final ReentrantLock lock = new ReentrantLock();
	private int size = 0;

  关于DelayedWorkQueue我们可以得出这样的结论:DelayedWorkQueue是基于堆的数据结构,按照时间顺序将每个任务进行排序,将待执行时间越近的任务放在在队列的队头位置,以便于最先进行执行。

3.2 ScheduledThreadPoolExecutor的使用

  ScheduledThreadPoolExecutor的执行主要分为两部分:

  • 1、当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时,会向ScheduledThreadPoolExecutor的DelayQueue添加一个实现了RunnableScheduledFutur接口的ScheduledFutureTask。
  • 2、线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。
3.2.1 执行任务的方法

  ScheduledThreadPoolExecutor实现了ScheduledExecutorService接口,该接口定义了可延时执行异步任务和可周期执行异步任务的特有功能,相应的方法分别为:

	//达到给定的延时时间后,执行任务。这里传入的是实现Runnable接口的任务,
	//因此通过ScheduledFuture.get()获取结果为null
	public ScheduledFuture<?> schedule(Runnable command,
	                                       long delay, TimeUnit unit);

	//达到给定的延时时间后,执行任务。这里传入的是实现Callable接口的任务,
	//因此,返回的是任务的最终计算结果
	public <V> ScheduledFuture<V> schedule(Callable<V> callable,
	                                           long delay, TimeUnit unit);

	//是以上一个任务开始的时间计时,period时间过去后,检测上一个任务是否执行
	//完毕,如果上一个任务执行完毕,则当前任务立即执行,如果上一个任务没有执
	//行完毕,则需要等上一个任务执行完毕后立即执行
	public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
	                                                  long initialDelay,
	                                                  long period,
	                                                  TimeUnit unit);

	//当达到延时时间initialDelay后,任务开始执行。上一个任务执行结束后到下一次
	//任务执行,中间延时时间间隔为delay。以这种方式,周期性执行任务。
	public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
	                                                     long initialDelay,
	                                                     long delay,
	                                                     TimeUnit unit);
3.2.2 执行过程

  ScheduledThreadPoolExecutor最大的特色是能够周期性执行异步任务
  ScheduledThreadPoolExecutor有两个内部类ScheduledFutueTask和DelayedWorkQueue,实际上这也是线程池工作流程中最重要的两个关键因素:任务以及阻塞队列。

  • ScheduledThreadPoolExecutor中的线程执行周期任务的过程
  1. 线程1从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务是指ScheduledFutureTask的time大于等于当前时间。
  2. 线程1执行这个ScheduledFutureTask。
  3. 线程1修改ScheduledFutureTask的time变量为下次将要被执行的时间。
  4. 线程1把这个修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。
  • schedule
      现在来看下ScheduledThreadPoolExecutor提交一个任务后,整体的执行过程。以ScheduledThreadPoolExecutor的schedule方法为例,源码:
	public ScheduledFuture<?> schedule(Runnable command,
	                                   long delay,
	                                   TimeUnit unit) {
	    if (command == null || unit == null)
	        throw new NullPointerException();
		//将提交的任务转换成ScheduledFutureTask
	    RunnableScheduledFuture<?> t = decorateTask(command,
	        new ScheduledFutureTask<Void>(command, null,
	                                      triggerTime(delay, unit)));
	    //延时执行任务ScheduledFutureTask
		delayedExecute(t);
	    return t;
	}

  方法很容易理解,为了满足ScheduledThreadPoolExecutor能够延时执行任务和能周期执行任务的特性,会先将实现Runnable接口的类转换成ScheduledFutureTask。

  • ScheduledFutureTask
      ScheduledFutureTask主要包含3个成员变量:
	//表示这个任务将要被执行的具体时间
	private long time;
	//表示这个任务被添加到ScheduledThreadPoolExecutor中的序号
	private final long sequenceNumber;
	//表示任务执行的间隔周期
	private final long period;

  decorateTask方法会将传入的Runnable转换成ScheduledFutureTask类。由于任何线程执行任务,总会调用run()方法。为了保证ScheduledThreadPoolExecutor能够延时执行任务以及能够周期性执行任务,ScheduledFutureTask重写了run方法:

	public void run() {
	    boolean periodic = isPeriodic();
	    if (!canRunInCurrentRunState(periodic))
	        cancel(false);
	    else if (!periodic)
			//如果不是周期性执行任务,则直接调用run方法
	        ScheduledFutureTask.super.run();
			//如果是周期性执行任务的话,需要重设下一次执行任务的时间
	    else if (ScheduledFutureTask.super.runAndReset()) {
	        setNextRunTime();
	        reExecutePeriodic(outerTask);
	    }
	}

  在重写的run方法中会先if (!periodic)判断当前任务是否是周期性任务,如果不是的话就直接调用run()方法;否则的话执行setNextRunTime()方法重设下一次任务执行的时间,并通过reExecutePeriodic(outerTask)方法将下一次待执行的任务放置到DelayedWorkQueue中。
  因此,可以得出结论:ScheduledFutureTask最主要的功能是根据当前任务是否具有周期性,对异步任务进行进一步封装。如果不是周期性任务(调用schedule方法)则直接通过run()执行,若是周期性任务,则需要在每一次执行完后,重设下一次执行的时间,然后将下一次任务继续放入到阻塞队列中。

  • delayedExecute
      接着会调用delayedExecute方法进行执行任务,这个方法也是关键方法,来看下源码:
	private void delayedExecute(RunnableScheduledFuture<?> task) {
	    if (isShutdown())
			//如果当前线程池已经关闭,则拒绝任务
	        reject(task);
	    else {
			//将任务放入阻塞队列中
	        super.getQueue().add(task);
	        if (isShutdown() &&
	            !canRunInCurrentRunState(task.isPeriodic()) &&
	            remove(task))
	            task.cancel(false);
	        else
				//保证至少有一个线程启动,即使corePoolSize=0
	            ensurePrestart();
	    }
	}
  • ensurePrestart
      该方法的重要逻辑会是在ensurePrestart方法中,它的源码为:
	void ensurePrestart() {
	    int wc = workerCountOf(ctl.get());
	    if (wc < corePoolSize)
	        addWorker(null, true);
	    else if (wc == 0)
	        addWorker(null, false);
	}

  关键在于它所调用的addWorker方法,该方法主要功能:新建Worker类,当执行任务时,就会调用被Worker所重写的run方法,进而会继续执行runWorker方法。在runWorker方法中会调用getTask方法从阻塞队列中不断的去获取任务进行执行,直到从阻塞队列中获取的任务为null的话,线程结束终止。

3.2.4 使用示例
public class ScheduledThreadPoolExecutorTest implements Runnable {
	 
    private ScheduledExecutorService scheduledExecutorService;
 
    private void showTime() {
        scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
        System.out.println(Thread.currentThread().getName() + ": " + new Date());
        scheduledExecutorService.schedule(new ScheduledThreadPoolExecutorTest(), 10, TimeUnit.SECONDS);
        scheduledExecutorService.shutdown();
    }
 
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": " + new Date());
    }
 
    public static void main(String[] args) {
        new ScheduledThreadPoolExecutorTest().showTime();
    }
}

  结果示例:

main: Mon Oct 18 21:24:13 CST 2021
pool-1-thread-1: Mon Oct 18 21:24:23 CST 2021

四、线程池理论

4.1 线程池工作原理*

  通过ThreadPoolExecutor创建线程池后,可以通过execute提交任务,execute方法源码:

	public void execute(Runnable command) {
	    if (command == null)
	        throw new NullPointerException();
	        
	    int c = ctl.get();
		//如果线程池的线程个数少于corePoolSize则创建新线程执行当前任务
	    if (workerCountOf(c) < corePoolSize) {
	        if (addWorker(command, true))
	            return;
	        c = ctl.get();
	    }
		//如果线程个数大于corePoolSize或者创建线程失败,则将任务存放在阻塞队列workQueue中
	    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);
	    }
		//如果当前任务无法放进阻塞队列中,则执行拒绝策略
	    else if (!addWorker(command, false))
	        reject(command);
	}

  execute方法执行逻辑:

  1. 如果当前运行的线程少于corePoolSize,则会创建新的线程来执行新的任务
  2. 如果运行的线程个数等于或者大于corePoolSize,则会将提交的任务存放到阻塞队列workQueue中
  3. 如果当前workQueue队列已满并且线程个数未超过maximumPoolSize,则会创建新的线程来执行任务
  4. 如果线程个数已经超过了maximumPoolSize,则会使用饱和策略RejectedExecutionHandler来进行处理

  线程池的设计思想就是使用了核心线程池corePoolSize、阻塞队列workQueue和线程池线程最大个数maximumPoolSize所组成的缓存策略来处理任务。
  execute方法的源码,其实就是线程池的工作原理:

  • Java线程池工作过程
  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用execute()方法添加一个任务时,线程池会做如下判断:

  a) 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
  b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
  c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  d) 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,则执行饱和策略。

  1. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  2. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

4.2 线程池的生命周期

  • 1、RUNNING
      线程池一旦被创建,就处于RUNNING状态,任务数为0,能够接收新任务,对已排队的任务进行处理。
  • 2、SHUTDOWN
      不接收新任务,但能处理已排队的任务。调用线程池的shutdown()方法,线程池由RUNNING转变为SHUTDOWN状态。
  • 3、STOP
      不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow()方法,线程池由(RUNNING或SHUTDOWN) 转变为STOP状态
  • 4、TIDYING
      所有的任务都已终止了,workerCount (有效线程数) 为0。进入TIDYING状态的两种方式:
  1. SHUTDOWN状态下,任务数为0, 其他所有任务已终止,线程池会变为TIDYING状态
  2. STOP状态下,线程池中执行中任务为空时,就会由STOP转变为TIDYING状态。
  • 5、TERMINATED线程池彻底终止。线程池在TIDYING状态执行完terminated()方法就会由TIDYING转变为TERMINATED状态。线程池中的terminated()方法是空实现,可以重写该方法进行相应的处理。

  线程池的生命周期图示:

  RUNNING -> SHUTDOWN:显式调用shutdown()方法, 或者隐式调用了finalize()方法。
  (RUNNING or SHUTDOWN) -> STOP:显式调用shutdownNow()方法。
  SHUTDOWN -> TIDYING:当线程池和任务队列都为空的时候。
  STOP -> TIDYING:当线程池为空的时候。
  TIDYING -> TERMINATED:当terminated()方法执行完成时候。

4.3 线程池死锁

  如果线程池中执行的任务在其执行过程中又会向同一个线程池提交另一个人任务,而前一个任务的执行结束又依赖后一个任务的执行结果,那么就有可能出现这样的情形:线程池中的所有工作者线程都处于等待其他任务的处理结果而这些任务仍在工作队列中等待执行,由于线程池中已经没有可以对工作队列中的任务进行处理的工作者线程,这种等待就会一直持续下去从而形成死锁。
  因此,适合提交给同一线程池实例执行的任务是相互独立的任务,而不是彼此有依赖关系的任务。对于彼此存在依赖关系的任务,可以考虑使用不同的线程池实例来执行这些任务

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值