27.线程管理:线程池

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_25991865/article/details/87992549

1.定义

线程是一种昂贵的资源,其开销有很多方面。因此,从整个系统乃至整个主机的角度来看我们需要一种有效使用线程的方式。线程池就是有效使用线程的一种常见方式。
线程池内部可以预先创建一定数量的工作者线程,客户端代码并不需要向线程池借用线程而是将其需要执行的任务作为一个对象提交给线程池,线程池可能将这些任务缓存在队列(工作队列)之中,而线程池内部的各个工作者线程则不断地从队列中取出任务并执行之。因此,线程池可以被看作基于生产者—消费者模式的一种服务,该服务内部维护的工作者线程相当于消费者线程,线程池的客户端线程相当于生产者线程,客户端代码提交给线程池的任务相当于“产品”,线程池内部用于缓存任务的队列相当于传输通道。
在这里插入图片描述

2.ThreadPoolExecutor

java.util.concurrent.ThreadPoolExecutor 类就是一个线程池,客户端代码可以调用ThreadPoolExecutor.submit 方法向其提交任务,ThreadPoolExecutor.submit方法声明如下:

public Future<?> submit(Runnable task)

线程池内部维护的工作者线程的数量就被称为该线程池的线程池大小。ThreadPoo!Executor 的线程池大小有 3 种形态:当前线程池大小表示线程池中实际工作者线程的数量;最大线程池大小表示线程池中允许存在的工作者线程的数量上限,核心线程大小表示一个不大于最大线程池大小的工作者线程数量上限 。
ThreadPoolExecutor 的构造器中包含参数数量最多的一个构造器的声明如下:

public ThreadPoolExecutor(int corePoolSize,
	int maximumPoolSize,
	long keepAliveTime,
	TimeUnit unit,
	BlockingQueue<Runnable> workQueue,
	ThreadFactory threadFactory,
	RejectedExecutionHandler handler)
  • workQueue 是被称为工作队列的阻塞队列,它相当于生产者—消费者模式中的传输通道。
  • corePoolSize 用于指定线程池核心大小。
  • maximumPoolSize 用于指定最大线程池大小。
  • keepAliveTime 和 unit 合在一起用于指定线程池中空闲线程的最大存活时间 。
  • threadFactory 指定用于创建工作者线程的线程工厂。
  • handler 参数用于封装被拒绝的任务的处理策略。

在初始状态下,客户端每提交一个任务线程池就创建一个工作者线程来处理该任务。随着客户端不断地提交任务,当前线程池大小也相应增加 。 在当前线程池大小达到核心线程池大小的时候,新来的任务会被存入工作队列之中。这些缓存的任务由线程池中的所有工作者线程负责取出进行执行。 线程池将任务存人工作队列的时候调用的是BlockingQueue 的非阻塞方法 offer(E e), 因此工作队列满并不会使提交任务的客户端线程暂停。当工作队列满的时候,线程池会继续创建新的工作者线程,直到当前线程池大小达到最大线程池大小。线程池是通过调用threadFactory.newThread 方法来创建工作者线程的。如果我们在创建线程池的时候没有指定线程工厂(即调用了 ThreadPoolExecutor 的其他构造器),那么ThreadPoo!Executor 会使用 Executors.defaultThreadFactory)所返回的默认线程工厂 。 当线程池饱和时,即工作者队列满并且当前线程池大小达到最大线程池大小的情况下,客户端试图提交的任务会被拒绝。为了提高线程池的可靠性,Java 标准库引入了 一个 RejectedExecutionHandler 接口用于封装被拒绝的任务的处理略,该接口仅定义了如下方法:

void rejectedExecution(Runnable r,ThreadPoolExecutor executor)

其中,r 代表被拒绝的任务,executor 代表拒绝任务 r 的线程池实例。我们可以通过线程池的构造器参数 handler 或者线程池的 setRejectedExecutionHandler(RejectedExecutionHandlerhandler)方法来为线程池关联一个 RejectedExecutionHandler。当客户端提交的任务被拒绝时,线程池所关联的RejectedExecutionHandler 的 rejectedExecution 方法会被线程池调用。 ThreadPoolExecutor 自身提供了几个现成的 RejectedExecutionHandler 接口:
在这里插入图片描述
ThreadPoolExecutor.AbortPolicy 是 ThreadPoolExecutor 使用的默认RejectedExecutionHandler。如果默认的 RejectedExecutionHandler (它会直接抛出异常)无法满足要求,那么我们可以优先考虑 ThreadPoolExecutor 自身提供的其他RejectedExecutionHandler, 其次才去考虑使用自行实现的RejectedExecutionHandler接口。
在当前线程池大小超过线程池核心大小的时候,超过线程池核心大小部分的工作者线程空闲(即工作者队列中没有待处理的任务)时间达到 keepAliveTime 所指定的时间后就会被清理掉,即这些工作者线程会自动终止并被从线程池中移除 。ThreadPoolExecutor.prestartAllCoreThreads() 则使得我们可以使线程池在未接收到任何任务的情况下预先创建并启动所有核心线程,这样可以减少任务被线程池处理时所需的等待时间(等待核心线程的创建与启动)。
ThreadPoolExecutor.shutdown()/shutdownNow() 方法可用来关闭线程池。使用shutdown()关闭线程池的时候,已提交的任务会被继续执行,而新提交的任务会像线程池饱和时那样被拒绝掉。ThreadPoolExecutor.shutdown()返回的时候线程池可能尚未关闭,即线程池中可能还有工作者线程正在执行任务。应用代码可以通过调用 ThreadPoolExecutor.awaitTermination(long timeout,TimeUnit unit)来等待线程池关闭结束。使用 ThreadPoolExecutor.shutdownNow()关闭线程池的时候,正在执行的任务会被停止,已提交而等待执行的任务也不会被执行。该方法的返回值是已提交而未被执行的任务列表,这为被取消的任务的重试提供了一个机会。由于ThreadPoo!Executor.shutdownNow() 内部是通过调用工作者线程的 interrupt 方法来停止正在执行的任务的,因此某些无法响应中断的任务可能永远也不会停止。反过来说,在关闭线程池的时候如果我们能够确保巳经提交的任务都已执行完毕并且没有新的任务会被提交,那么调用 ThreadPoolExecutor.shutdownNow()总是安全可靠的。

3.任务的处理结果、异常处理与取消

如果客户端关心任务的处理结果,那么它可以使用 ThreadPoolExecutor 的另外一个 submit 方法来提交任务,该 submit 方法的声明如下:

public <T> Future<T> submit(Callable<T> task)

task 参数代表客户端需要提交的任务,其类型为 java.util.concurrent.Callable。Callable接口定义的唯一方法声明如下:

V call() throws Exception

Callable 接口也是对任务的抽象:任务的处理逻辑可以在 Callable 接口实现类的 call方法中实现。 Callable 接口相当于一个增强型的 Runnable 接口: call 方法的返回值代表相应任务的处理结果,其类型 V 是通过 Callable 接口的类型参数指定的; call 方法代表的任务在其执行过程中可以抛出异常。而 Runnable 接口中的 run 方法既无返回值也不能抛出异常。Executors.callab/e(Runnable task,T result)能够将 Runnable 接口转换为 Callable 接口实例。
上述 submit 方法的返回值类型为 java.util.concurrent.Future 。 Future接口实例可被看作提交给线程池执行的任务的处理结果句柄 (Handle), Future.get()方法可以用来获取 task参数所指定的任务的处理结果,该方法声明如下:

V get() throws InterruptedException,ExecutionException

Future.get()被调用时,如果相应的任务尚未执行完毕,那么 Future. get()会使当前线程暂停,直到相应的任务执行结束(包括正常结束和抛出异常而终止)。 因此, Future.get()是个阻塞方法,该方法能够抛出 InterruptedException 说明它可以响应线程中断。另外,假设相应的任务执行过程中抛出一个任意的异常originalException, 那么 Future.get()方法本身就会抛出相应的 ExecutionException 异常。调用这个异常 (ExecutionException) 的getCause()方法可返回originalException 。因此,客户端代码可以通过捕获 Future.get()调用抛出的异常来知晓相应任务执行过程中抛出的异常。
由于在任务未执行完毕的情况下调用 Future.get()方法来获取该任务的处理结果会导致等待并由此导致上下文切换,因此客户端代码应该尽可能早地向线程池提交任务,并尽可能晚地调用 Future.get()方法来获取任务的处理结果,而线程池则正好利用这段时间来执行已提交的任务(包括我们关心的任务)。
Future 接口还支持任务的取消。为此,Future 接口定义了如下方法:

boolean cancel(boolean mayInterruptIfRunning)

该方法的返回值表示相应的任务取消是否成功。任务取消失败的原因包括待取消的任务已执行完毕或者正在执行、已经被取消以及其他无法取消因素。参数maylnterruptlfRunning 表示是否允许通过给相应任务的执行线程发送中断来取消任务。Future.isCancelled()返回值代表相应的任务是否被成功取消。由于一个任务被成功取消之后,相应的 Future.get()调用会抛出 CancellationException 异常(运行时异常),因此如果任务有可能会被取消,那么在获取任务的处理结果之前,我们需要先判断任务是否已经被取消了。
Future.isDone()方法可以检测相应的任务是否执行完毕。任务执行完毕、执行过程中抛出异常以及任务被取消都会导致该方法返回 true 。
Future.get()会使其执行线程无限制地等待,直到相应的任务执行结束。商用系统中这种无时间限制的等待往往是不现实的。此时我们可以使用 get 方法的另外一个版本,其声明如下:

v get(long timeout, TimeUnit unit)throws InterruptedException,
ExecutionException,TimeoutException

该方法的作用与Future.get()相同,不过它允许我们指定一个等待超时时间。如果在该时间内相应的任务未执行结束,那么该方法就会抛出 TimeoutException。

4.线程池监控

ThreadPoolExecutor 类提供了对线程池进行监控的相关方法。
在这里插入图片描述
此外, ThreadPoolExecutor 提供的两个钩子方法 (Hook Method): beforeExecute(Thread t,Runnable r)和 afterExecute(Thread t,Runnable r)也能够用于实现监控。设 executor 为任意一个 ThreadPoolExecutor 实例,在任意一个任务 r 被线程池 executor 中的任意一个工作者线程 t 执行前,executor.beforeExecute(t,r)会被执行;当 t 执行完 r 之后,不管 r 的执行是否是成功的还是抛出了异常, executor.afterExecute(t,r)始终会被执行 。 因此,如果有必要的话我们可以通过创建 ThreadPoolExecutor 的子类并在子类的 beforeExecute/afterExecute方法实现监控逻辑,比如计算任务执行的平均耗时。

5.线程池死锁

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

6.工作者线程的异常终止

  • 如果任务是通过 ThreadPoolExecutor.submit 调用提交给线程池的,那么这些任务在其执行过程中即便是抛出了未捕获的异常也不会导致对其进行执行的工作者线程异常终止 。这种情形下任务所抛出的异常可以通过 Future.get()所抛出的ExecutionException 来获取。
  • 如果任务是通过 ThreadPoolExecutor.execute 方法提交给线程池的,那么这些任务在其执行过程中一旦抛出了未捕获的异常,则对其进行执行的工作者线程就会异常终止。可以通过 ThreadPoolExecutor 的构造器参数或者 ThreadPoolExecutor.setThreadFactory 方法为线程池关联一个线程工厂。在这个线程工厂里面我们可以为其创建的工作者线程关联一个 UncaughtExceptionHandler, 通过这个关联的 UncaughtExceptionHandler 我们可以侦测到任务执行过程中抛出的未捕获异常 。 不过,由于 ThreadPoolExecutor 内部实现的原因,只有通过 ThreadPoolExecutor.execute 调用(而不是 ThreadPoolExecutor.submit 调用)提交给线程池执行的任务,其执行过程中抛出的未捕获异常才会导致 UncaughtExceptionHandler.uncaughtException 方法被调用。

猜你喜欢

转载自blog.csdn.net/sinat_25991865/article/details/87992549