多线程(三)线程池

文章目录

前言 为什么要用线程池

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

  • 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的有许多构造方法,可以通过参数最多的构造方法来理解创建线程池有哪些需要配置的参数。参数最多(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事先启动核心线程。

1.2.2 maximumPoolSize

  线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。

1.2.3 keepAliveTime

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

1.2.4 unit

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

1.2.5 workQueue

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

1.2.6 threadFactory

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

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将被拒绝的任务返回给调用者处理
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。
  此处创建一个统一的线程任务,方便测试四种线程池:

	public class MyRunnable implements Runnable {
	    @Override
	    public void run() {
	        System.out.println(Thread.currentThread().getName() + " is running...");
	    }
	}

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线程池执行任务的流程:

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

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

	public class FixedThreadPoolTest {
	
	    public static void main(String[] args) {
	        ExecutorService executorService = Executors.newFixedThreadPool(2);
	        MyRunnable myRunnable = new MyRunnable();
	        for (int i = 0; i < 5; i++) {
	            executorService.execute(myRunnable);
	        }
	
	        System.out.println("线程任务开始执行");
	        executorService.shutdown();
	    }
	}

  结果:

线程任务开始执行
pool-1-thread-1 is running…
pool-1-thread-1 is running…
pool-1-thread-2 is running…
pool-1-thread-1 is running…
pool-1-thread-2 is running…

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用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。示例:

	public class SingleThreadExecutorTest {
	
	    public static void main(String[] args) {
	        ExecutorService executorService = Executors.newSingleThreadExecutor();
	        MyRunnable myRunnable = new MyRunnable();
	        for (int i = 0; i < 5; i++) {
	            executorService.execute(myRunnable);
	        }
	
	        System.out.println("线程任务开始执行");
	        executorService.shutdown();
	    }
	}

  结果:

线程任务开始执行
pool-1-thread-1 is running…
pool-1-thread-1 is running…
pool-1-thread-1 is running…
pool-1-thread-1 is running…
pool-1-thread-1 is running…

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,这个队列的作用就是传递任务,并不会保存
  因此当提交任务的速度大于处理任务的速度时,每次提交一个任务,就会创建一个线程。极端情况下会创建过多的线程,耗尽CPU和内存资源。
  它的执行流程:

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

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

	public class CachedThreadPoolTest {
	
	    public static void main(String[] args) {
	        ExecutorService executorService = Executors.newCachedThreadPool();
	        MyRunnable myRunnable = new MyRunnable();
	        for (int i = 0; i < 5; i++) {
	            executorService.execute(myRunnable);
	        }
	
	        System.out.println("线程任务开始执行");
	        executorService.shutdown();
	    }
	}

  结果:

线程任务开始执行
pool-1-thread-1 is running…
pool-1-thread-4 is running…
pool-1-thread-2 is running…
pool-1-thread-5 is running…
pool-1-thread-3 is running…

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中获取time大于等于当前时间的ScheduledFutureTask;
  2. 执行完后修改这个task的time为下次被执行的时间;
  3. 然后再把这个task放回队列中。

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

public class ScheduledThreadPoolTest {

    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService =
        	Executors.newScheduledThreadPool(3);
        MyRunnable myRunnable = new MyRunnable();
        for (int i = 0; i < 5; i++) {
            // 参数1:目标对象,参数2:隔多长时间开始执行线程,参数3:执行周期,参数4:时间单位
            scheduledExecutorService
            	.scheduleAtFixedRate(myRunnable, 1, 2, TimeUnit.SECONDS);
        }
        System.out.println("线程任务开始执行");
    }
}

  结果:

线程任务开始执行
pool-1-thread-1 is running…
pool-1-thread-2 is running…
pool-1-thread-1 is running…
pool-1-thread-3 is running…
pool-1-thread-2 is running…
pool-1-thread-1 is running…
pool-1-thread-3 is running…
pool-1-thread-2 is running…
pool-1-thread-1 is running…
pool-1-thread-3 is running…

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

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

  • newFixedThreadPool和newSingleThreadExecutor
      主要问题是堆积的请求处理队列(队列最大值是Integer.MAX_VALUE)可能会耗费非常大的内存,甚至OOM。
  • newCachedThreadPool 和 newScheduledThreadPool
    主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

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

1.5 线程池阻塞队列

  阻塞队列的作用:用来存储等待执行的任务。阻塞队列中的任务也FIFO特性的,即先入队,先执行。
  常见阻塞队列有4种,此处先简单了解,后续有专门章节进行介绍。具体的队列:

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

1.6 线程池的拒绝策略

  如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务,说明线程池处于饱和状态,此时必须采取一种策略处理新提交的任务,这种策略就是拒绝策略。
  5种拒绝策略:

  • 1、ThreadPoolExecutor.AbortPolicy
      ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy,直接抛出RejectedExecutionException异常
  • 2、ThreadPoolExecutor.CallerRunsPolicy
      在任务被拒绝添加后,会用调用execute函数的上层线程,让调用者去处理。
  • 3、ThreadPoolExecutor.DiscardPolicy
      不处理新任务,直接丢弃。不会抛异常也不会执行。
  • 4、ThreadPoolExecutor.DiscardOldestPolicy
      当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去重试
  • 5、自定义拒绝策略
      比如想让被拒绝的任务在一个新的线程中执行,示例:
static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        new Thread(r,"新线程"+new Random().nextInt(10)).start();
    }
}

  此处以AbortPolicy为例,看一下拒绝策略的效果:

public class ThreadPoolTest{
	public static void main(String[] args) {  
	    //创建一个可重用固定线程数的线程池  
	    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
	    	5, 5, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
	    //创建线程  
	    Thread t1 = new MyThread();  
	    Thread t2 = new MyThread();  
	    Thread t3 = new MyThread();  
	    Thread t4 = new MyThread();  
	    Thread t5 = new MyThread();  
	    Thread t6 = new MyThread();  
	    Thread t7 = new MyThread();  
	    //将线程放入池中进行执行  
	    threadPoolExecutor.execute(t1);  
	    threadPoolExecutor.execute(t2);  
	    threadPoolExecutor.execute(t3);  
	    threadPoolExecutor.execute(t4);  
	    threadPoolExecutor.execute(t5); 
	    threadPoolExecutor.execute(t6);
	    threadPoolExecutor.execute(t7);
	    threadPoolExecutor.shutdown();
	}  
}  

class MyThread extends Thread {  
	@Override  
	public void run() {  
	    System.out.println(Thread.currentThread().getName() + "正在执行。。。");  
	}  
}

  在上述代码中,线程池中最大线程数是5,阻塞队列数量是1,所以当第7个线程进入线程池时,如果前6个线程都在执行任务,那么就需要拒绝策略来处理第7个线程了。结果示例:

1.7 线程池参数配置

  要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析:

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

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

  • 1、CPU密集型任务
      CPU密集型任务配置尽可能少的线程数量,如配置CPU个数+1的线程数的线程池。
  • 2、IO密集型任务
      IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如配置两倍CPU个数+1。
      IO密集型任务另一种配置经验:线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间
      例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式:4 * 100% * 100% / 50% = 8。
      例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式:4 * 100% * 100% / 10% = 40。

  混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务。设备的CPU个数可以通过Runtime.getRuntime().availableProcessors()方法获得。
  优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
  执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
  依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
  同时,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃

  IO型任务与计算型任务:要根据任务的“轻重缓急”来指定线程池的核心参数,包括线程数、回收策略和任务队列:对于执行比较慢、数量不大的IO型任务,或许要考虑更多的线程数,而不需要太大的队列。而对于吞吐量较大的计算型任务,线程数量不宜过多,可以是CPU核数或核数 2(理由是,线程一定调度到某个CPU进行执行,如果任务本身是CPU绑定的任务,那么过多的线程只会增加线程切换的开销,并不能提升吞吐量),但可能需要较长的队列来做缓冲。

1.8 线程池的监控

1.8.1 获取ThreadPoolExecutor的使用情况

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

  • taskCount
      线程池需要执行的任务数量。可以通过如下方法获取:
	public long getTaskCount()
  • completedTaskCount
      线程池在运行过程中已完成的任务数量,小于或等于taskCount。可以通过如下方法获取:
	public long getCompletedTaskCount()
  • largestPoolSize
      线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。可以通过如下方法获取:
	public int getLargestPoolSize()
  • 线程池的线程数量
      如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。可以通过如下方法获取:
	public int getPoolSize()
  • 活动的线程数
      可以通过如下方法获取:
	public int getActiveCount()
1.8.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.9 线程池相关问题

1.9.1 线程数过多会造成什么异常
  • 1、线程的生命周期开销非常高
  • 2、消耗过多的 CPU 资源
      如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。
  • 3、降低稳定性
      JVM在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM的启动参数、Thread构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError异常。
1.9.2 ThreadPoolExecutor中监控的方法
	//线程池已执行和未执行的任务总数
	public long getTaskCount()
	//已完成的任务数量
	public long getCompletedTaskCount()
	//线程池当前的线程数量
	public int getPoolSize()
	//线程池核心线程数
	public int getCorePoolSize()
	//当前线程池中正在执行任务的线程数量
	public int getActiveCount()
1.9.3 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
  • 1、高并发、任务执行时间短的业务
      线程池线程数可以设置为CPU核数+1,减少线程上下文的切换。
  • 2、并发不高、任务执行时间长的业务
      假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务。
      假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换。
  • 3、并发高、业务执行时间长
      解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。
      最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

二、线程池的使用

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方法既无返回值也不能抛出异常。
	 * Executors.callable(Runnable task,T result)方法,可以将Runnable接口转换
	 * 为Callable接口实例
	 */
	<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(),此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞

2.2 FutureTask

  在向线程池中提交任务时用到了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。

  关于线程池的关闭,示例:

public class ThreadPoolTest{
	public static void main(String[] args) {  
	    // 创建一个可重用固定线程数的线程池  
	    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
	    	5, 5, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
	    // 创建线程  
	    Thread t1 = new MyThread();  
	    Thread t2 = new MyThread();  
	    Thread t3 = new MyThread();  
	    Thread t4 = new MyThread();  
	    Thread t5 = new MyThread();  
	    // 将线程放入池中进行执行  
	    threadPoolExecutor.execute(t1);  
	    threadPoolExecutor.execute(t2);  
	    threadPoolExecutor.execute(t3);  
	    threadPoolExecutor.execute(t4);  
	    threadPoolExecutor.execute(t5); 
	    System.out.println(threadPoolExecutor.isShutdown());
	    threadPoolExecutor.shutdown();
	    System.out.println(threadPoolExecutor.isShutdown());
	}  
}  

class MyThread extends Thread {  
	@Override  
	public void run() {  
	    System.out.println(Thread.currentThread().getName() + "正在执行。。。");  
	}  
}  

  此程序的运行结果并不唯一,不过两次调用threadPoolExecutor.isShutdown()结果是一样的,第一次是false,第二次是true。示例:

pool-1-thread-1正在执行。。。
pool-1-thread-3正在执行。。。
pool-1-thread-2正在执行。。。
false
pool-1-thread-4正在执行。。。
pool-1-thread-5正在执行。。。
true

三、ScheduledThreadPoolExecutor

  ScheduledThreadPoolExecutor是个特殊的线程池,其可以用来在给定延时后执行异步任务或者周期性执行任务。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方法的源码,其实就是线程池的工作原理:

  线程池使用示例:

public class ThreadPoolTest{
	public static void main(String[] args) {  
	    //创建一个可重用固定线程数的线程池  
	    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
	    	5, 5, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
	    //创建线程  
	    Thread t1 = new MyThread();  
	    Thread t2 = new MyThread();  
	    Thread t3 = new MyThread();  
	    Thread t4 = new MyThread();  
	    Thread t5 = new MyThread();  
	    //将线程放入池中进行执行  
	    threadPoolExecutor.execute(t1);  
	    threadPoolExecutor.execute(t2);  
	    threadPoolExecutor.execute(t3);  
	    threadPoolExecutor.execute(t4);  
	    threadPoolExecutor.execute(t5);  
	}  
}  

class MyThread extends Thread {  
	@Override  
	public void run() {  
	    System.out.println(Thread.currentThread().getName() + "正在执行。。。");  
	}  
}

  结果:

pool-1-thread-1正在执行。。。
pool-1-thread-3正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-4正在执行。。。
pool-1-thread-5正在执行。。。

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 线程池死锁

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

public class ThreadPoolTest{
	ExecutorService exec = Executors.newSingleThreadExecutor();

	/**
	 * 该任务会提交另外一个任务到线程池,并且等待任务的执行结果
	 */
	public class RenderPageTask implements Callable<String> {
		@Override
		public String call() throws Exception {
			System.out.println("RenderPageTask 依赖LoadFileTask任务返回的结果...");
			Future<String> header, footer;
			header = exec.submit(new LoadFileTask("header.html"));
			footer = exec.submit(new LoadFileTask("footer.html"));
			String page = renderBody();
			return header.get() + page + footer.get();
		}

		public String renderBody() {
			return "render body is ok.";
		}
	}

	public static void main(String[] args) {
		ThreadPoolTest lock = new ThreadPoolTest();
		Future<String> result = lock.exec.submit(lock.new RenderPageTask());
		try {
			System.out.println("last result:" + result.get());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
	}
}

class LoadFileTask implements Callable<String> {
	private String fileName;

	public LoadFileTask(String fileName) {
		this.fileName = fileName;
	}

	@Override
	public String call() throws Exception {
		System.out.println("LoadFileTask execute call...");
		return fileName;
	}
}

  结果:

RenderPageTask 依赖LoadFileTask任务返回的结果…

  例子中的线程池中只有一个线程,但是要先进入线程池的任务却要依赖后续任务的结果,就发生了死锁。

转载:https://blog.youkuaiyun.com/m0_37741420/article/details/120838550

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值