Java线程池

492 阅读14分钟

线程池是指管理一组同构工作线程的资源池。线程从工作队列中获取一个任务,执行任务,然后返回线程等待下一个任务。

为什么要使用线程池

在《阿里巴巴Java手册》中提到:

为每个任务分配一个线程存在缺陷,尤其是当需要大量创建线程,其原因有:

  1. 线程生命周期的开销非常高。因为线程的创建和销毁都是有开销的。
  2. 资源消耗。活跃的线程会消耗系统资源,尤其是内存。
  3. 稳定性。在可创建线程的数量上存在一个限制。如果破坏了这个限制,很可能抛出OutOfMemoryError异常。

也就是说,在一定范围内,增加线程可以提高系统的吞吐率,但是超过了这个范围,再创建更多的线程只会降低程序的执行速度。

合理的使用线程池有3个好处:

  1. 降低资源消耗。通过重复利用已经创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务不需要等待线程创建就能立即执行。将线程的创建和执行分离开来,进行了解耦。
  3. 提高了线程的可管理性。线程是稀缺资源,不能频繁创建。使用线程池可以统一分配、调优和监控。

Executor框架

Java的线程既是工作单元,也是执行机制。从JDK5开始,把二者分离开来。工作单元包括Runnable和Callable,而执行机制由Executor框架提供的。

Executor 框架是Java5之后引进的,在Java 5之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。

Executor框架的两级调度模型

Executor两级调度模型如下。

用户将多个任务提交给Executor框架,框架在线程池中分配线程执行它们。然后操作系统再将这些线程分配给处理器执行。


Executor框架结构

Executor框架由三大部分组成:

  1. 任务:Ruannable接口或者Callable接口
  2. 任务的执行:Executor接口以及继承该接口的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口,分别是ThreadPoolExecutor和ScheduledThreadPoolExecutor。
  3. 异步计算的结果,包括接口Future和实现该接口的FutureTask类。

具体介绍:

  • Executor是Executor框架的基础,将任务的提交与执行分离开
  • ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务
  • ScheduledThreadPoolExecutor是一个实现类,可以管理延迟任务或周期任务,比Timer更强大和灵活。
  • Future接口和FutureTask类,代表异步计算的结果。
  • Runnable和Callable接口的实现类都可以被线程池执行。

使用示意图如下所示:

  1. 主线程首先创建Runnable或者Callable接口的任务对象。使用Executors.callable(Runnable task)将Runnable对象封装为一个Callable对象。
    • Runnable是一种有局限的抽象,因为它不能返回值或者抛出一个受检查的异常。
    • Callable的call()方法类似于Runnable的run()方法,但是call()方法有返回值,并且可能抛出一个异常。
  2. 把任务对象交给ExecutorService执行。
    • execute()提交只能提交一个Runnable对象,且返回值是void。也就是说提交后如果线程运行,和主线程就脱离了关系。
    • submit()方法可以提交一个Callable接口的对象,使用这种方式提交会返回一个实现了Future接口的对象(FutureTask对象),代表了该线程的执行结果。
    • submit()方法提交Runnable对象,会返回null
  3. 主线程通过FutureTask对象得到异步执行的结果。
    • 主线程通过FutureTask.get()方法用来等待任务执行完成和获取执行结果。
      • 任务已经完成,返回结果或抛出异常
      • 任务未完成,阻塞直至完成
      • 任务抛出异常,get()方法将该异常封装为ExecutionExcepiton并重新抛出,可以使用getCause()来获取被封装的初始异常
      • 任务被取消,get()方法将抛出CancellationException
    • 主线程通过FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行

ThreadPoolExecutor

线程池的处理流程

  1. 线程池判断核心线程池里的线程是否都在执行任务。如果不是,创建一个新的工作线程来执行任务。否则,进入2
  2. 线程池判断工作队列是否已经满。如果没有,则将新提交的任务存储在工作队列中。否则,进入3
  3. 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。否则,说明线程池已满,交给饱和策略处理这个任务。

ThreadPoolExecutor参数

ThreadPoolExecutor(int corePoolSize,//线程池基本大小
                   int maximumPoolSize, //线程池最大尺寸
                   long keepAliveTime, //线程空闲后的存活时间
                   TimeUnit unit, 
                   BlockingQueue<Runnable> workQueue, //任务队列
                   ThreadFactory threadFactory, //线程工厂  
                   RejectedExecutionHandler handler) //饱和策略
  • corePoolSize是线程池的基本大小
    • 在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务。
    • 可以调用prestartAllCoreThreads()或者prestartCoreThread()方法预创建线程,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。
    • 默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
    • 在工作队列满了的情况下才会创建超出这个数量的线程。
  • maximumPoolSize是线程池的最大尺寸,表示可以同时活动的线程数量的上限。
  • keepAliveTime 和 unit 共同表示了线程空闲后的存活时间
    • 默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize。也就是当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。
    • 如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0
  • workQueue是一个阻塞队列,用于保存等待执行的任务。
    • 无界队列中,如果所有工作者线程都处于忙碌状态, 那么任务将在队列中等待。
    • 有界队列包括ArrayBlockingQueue以及有界的LinkedBlockingQueue和PriorityBlockingQueue。线程池小而队列较大,有助于减少内存使用量,降低CPU的使用率,同时还可以减少上下文切换,但是可能会限制吞吐量。
    • 除了上述两种队列,还有同步移交队列。对于非常大或无界的线程池,可以通过SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程。这个队列的put方法会阻塞,直到有线程准备从队列里面take,所以本质上SynchronousQueue并不是Queue,它不存储任何东西,它只是在移交东西,是一种在线程之间进行移交的机制。要将一个任务放到其中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建一个新的线程,否则这个任务将被拒绝。
  • threadFactory是一个线程工厂。线程池在创建线程时,通过线程工厂方法来完成。
  • handler是当ThreadPoolExecutor已经关闭,或者达到了最大线程池大小并且工作队列已满,execute()方法调用的Handler。有四种饱和策略。
    • AbortPolicy: 饱和策略,使用这种策略的线程池,将在无法继续接受新任务时,给任务提交方抛出RejectedExecutionException,让他们决定要如何处理
    • DiscardPolicy:抛弃策略,直接丢弃掉新来的任务
    • CallerRunsPolicy:调用者运行策略,这个策略,顾名思义,将把任务交给调用方所在的线程去执行
    • Discard-OldestPolicy: 抛弃最旧的策略,抛弃下一个奖杯执行的任务,然后尝试重新提交新的任务。(如果是优先级队列,会抛弃优先级最高的任务,最好不要一起使用)

生命周期

为了解决执行服务的生命周期问题,Executor拓展了ExecutorService接口,添加了一些用于生命周期管理的方法。ExecutorService的生命周期有三种状态:运行、关闭和已终止。

在初始创建时,ExecutorService处于运行状态。使用shutdown()方法将不再接受新的任务,同时等待提交的任务都执行完成之后再关闭。调用shutdownNow()方法,相当于调用了每个线程的interrupt()方法,将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。

在ThreadPoolExecutor中,定义了如下状态:

// runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;
  • RUNNING 是运行状态,可以接受执行队列中的任务
  • SHUTDOWN 是调用了shutdown()方法
  • STOP 是调用了shutdownNow()方法
  • TIDYING 是所有任务都执行完毕的状态,在调用shutdown()或shutdownNow()时都会尝试更新为这个状态
  • TERMINATED 是终止状态,当执行terminated()会更新为这个状态

ThreadPoolExecutor.execute()

  1. 如果当前运行的线程少于corePoolSize,即使有空闲线程,也会创建新线程来执行任务
  2. 如果运行的线程等于或者多于corePoolSize,或者线程创建失败,则将任务加入阻塞队列
  3. 如果线程池不处于运行中或任务无法放入队列(队列满),创建新的线程来执行任务
  4. 如果创建新线程将使得当前运行线程超过maximunPoolSize,任务将被拒绝,调用RejectedExecutionHandler.rejectExecution()

放到源码中分析如下:

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,成功将任务放到队列
        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);
    }

线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列中的任务来执行。

Execuotrs创建ThreadPoolExecutor

通过使用Executor框架的Executors,可以创建三种类型的ThreadPoolExecutor,下面分别介绍

1.FixedThreadPool

FixedThreadPool 是可重用固定线程数的线程池。

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

其corePoolSize和maximunPoolSize都被设置为相同的值。当线程池中线程池大于该值,keepAliveTime表示超过这个时间后多于的线程被终止,这里设置为0表示立即被终止。

execute()执行过程如下:

  1. 当前线程数少于 corePoolSize ,创建新线程执行任务
  2. 运行线程数等于 corePoolSize , 将任务加入 LinkedBlockingQueue
  3. 线程执行完1中任务,在循环中反复获取任务来执行

使用了 LinkedBlockingQueue 这一无界队列作为工作队列,有以下影响:

  1. 线程数达到 corePoolSize 会在队列中等待,线程数不会超过该值
  2. maximunPoolSize 和 keepAliveTime 是无效的。
  3. 运行中的线程池不会执行饱和策略(未执行shutdown()方法时)

2. SingleThreadExecutor

SingleThreadExecutor 是使用单个 worker 线程的 Executor。

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

和 FixedThreadPool 类似,两个 size 都设置为1,同样使用了 LinkedBlockingQueue。

3. CachedThreadPool

CachedThreadPool 是一个根据需要创建新线程的可缓存、没有规模限制的线程池。

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

其 corePoolSize 为空, maximunPoolSize 为 Integer.MAX_VALUE, 也就是线程池大小是无界的。 keepAliveTime 设为60秒, 也就是空闲线程等待新任务的最长时间为60秒,超过会被终止。 CachedThreadPool 使用没有容量的 SynchronousQueue 作为工作队列。 如果主线程提交任务的速度高于线程池中线程处理任务的速度,会不断创建新线程。极端情况下,会因为创建过多线程而耗尽CPU和内存资源。

execute()执行过程如下:

  1. 首先执行SynchronousQueue.offer(task),如果此时线程池中有空闲线程正在执行SynchronousQueue.poll(),那么这两个操作配对成功,主线程把任务交给空闲线程执行
  2. 若没有空闲线程,将没有线程执行SynchronousQueue.poll(),此时将创建一个新线程执行任务,任务完成后,会执行SynchronousQueue.poll(),该操作让空闲线程最多等待60秒,如果提交了新任务则执行;否则超时该线程被终止。

ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor, 用来在给定的延迟之后运行任务或者定期执行任务。

Timer用来负责管理延迟任务以及周期任务,但是它存在一些缺陷,应该使用与ScheduledThreadPoolExecutor来替代它。原因如下:

  • Timer类在执行所有定时任务时只会创建一个线程,当有多个定时任务时,就会产生延迟。
  • 当多个定时任务中有一个任务抛出异常,所有的任务都无法执行。
  • Timer执行周期任务时依赖系统时间。后者不会由于系统时间的改变而发生执行的变化。

FutureTask

FutureTask 实现了 Future 接口以及 Runnable 接口。因此, FutureTask 可以交给 Executor 执行,也可以由调用线程直接执行 FutureTask.run()

FutureTask的三种状态

  1. 未启动。创建了一个 FutureTask ,但是没有执行 run() 方法之前。
  2. 已启动。执行 run() 方法过程中。
  3. 已完成。分为正常结束,被取消而结束,以及抛出异常而结束。

如果 FutureTask 未启动或者已启动,执行 FutureTask.get() 方法将导致调用线程阻塞; 当处于已完成状态,执行该方法将导致线程立即返回或者抛出异常。

当 FutureTask 未启动,执行 FutureTask.cancel() 将导致此任务永远不会执行;当 FutureTask 已启动,执行 FutureTask.cancel(true) 将以中断执行此任务线程的方式试图停止任务;但是执行FutureTask.cancel(false)不会对正在执行此任务的线程产生影响;当已完成,执行该方法将返回false。

FutureTask实现

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

每一个基于 AQS 实现的同步器都包含两种类型的操作:

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

在FutureTask中声明了一个内部私有的继承于 AQS 的子类 Sync,对 FutureTask 公有方法的调用都会委托给这个内部子类。


参考资料