JAVA线程池

来源:互联网 发布:java static volatile 编辑:程序博客网 时间:2024/06/07 13:34

在java1.5之前的版本是没有线程池的,我们只能通过相关的容器自行开发代码来实现线程池功能。所幸从1.5开始,JDK提供了线程池的接口及其实现类,基本可以满足我们的使用需求。


线程池应用场景

在实际使用中,我们可能会涉及到大量线程的创建。比如,网站的某些请求,每个请求需要一个线程来支持,可能需要同时创建大量的线程。假设创建一个线程所需的实践是T1,线程执行时间是T2,线程的销毁时间是T3。如果T2>>T1+T3,线程的创建和销毁时间可以忽略不记,这时大量的线程创建、销毁对于整个程序没有什么大的影响;但是如果T2<<T1+T3,创建、销毁时间就会严重的影响到程序的执行时间。如果我们存在有大量的类似的线程,那么我们就必须考虑减少线程的创建、销毁的开销。

我们采用类似于数据库连接池的思想,提出了线程池概念来解决这样的情况。线程池包含一定数量的线程,有任务时,就去线程池查找是否有空闲线程,如果有就执行,没有空闲线程的话就在任务队列中等待直到有线程空闲出来。

这样做的话,可以减少线程创建、销毁的开销。同时由于线程池中线程数量是一定的,也避免了线程数量不受控制导致的内存溢出异常。不必担心执行的线程少了,会导致效率降低,只要确保CPU的利用率,速度是有保证的。


线程池实现原理

一个可以满足绝大多数要求的线程池,我认为至少应该包含以下4个部分:

1、线程池管理器:用于创建和管理线程池,并添加任务;

2、工作线程:线程池中的线程,由线程池自动创建,由工作或是空闲2中状态;

3、任务接口:每个任务都需要实现该接口,工作线程就是通过该接口来实现具体的工作。其实是Runnable接口,当然我们也可以自己封装一些更具有业务含义的接口出来。

4、任务队列:在任务找不到空闲的工作线程时,就会被置于任务队列中等待有工作线程释放。实现时可以根据不同的情况来设置队列的优先关系。

线程池实现

1.5之前我们都是自己来实现这些功能,现在我们自己实现这些东西。1.5及其后的版本我们可以直接使用JDK提供的接口或是直接使用其实现类。

由于现在很少再使用1.5之前的JDK了,所以就不在讨论如何自己实现线程池,我们着重的讨论一下JDK所提供的。


JDK提供的线程池

Executor

public interface Executor {    void execute(Runnable command);}


执行已提交的Runnable任务。是线程池的顶级接口。

ExecutorService

public interface ExecutorService extends Executor {……}


继承实现了Executor接口。是线程池的主要接口,提供了管理、终止的方法,以及为了跟踪一个或多个异步任务执行状况而生成Future的方法。

可以关闭ExecutorService,这将导致其停止接受新任务。关闭后,执行程序将最后终止,这时没有任务在执行,也没有任务在等待,并且无法提交新任务。

1)void shutdown();线程池的状态则立刻变成SHUTDOWN状态。此时,不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。

当shutdown执行时,会顺序将线程池中处于等待状态的线程终止。至于执行线程则会等待它们执行完成。

2)List<Runnable> shutdownNow();线程池的状态立刻变成STOP状态,试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。无法保证能够停止正在处理的活动执行任务,但是会尽力尝试。

使用Thread.interrupt()来终止线程,但不保证成功。

使用sublimt提交任务,invokeAll和invokeAny是批量执行的方法。

AbstractExecutorService

public abstract class AbstractExecutorService implements ExecutorService {……}


实现ExecutorService接口的部分方法,如:submit、invokeAny和invokeAll。

ThreadPoolExecutor

public class ThreadPoolExecutor extends AbstractExecutorService {……}


这是线程池的实现类,我们可以通过构造函数中参数来设置不同功能的线程池,当然为了简便起见,JDK提供了工具类Executors,并预置了几种常见的线程池类型:

  • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

以上是常见的几种能模式,我们也可以通过手工修改参数的方式对类进行调整和配置。我们可以参考一下参数:

  • 核心和线程池大小

线程池根据corePoolSize(线程池要求活跃的最小线程数)和maximumPoolSize(线程池支持的最大线程数)设置的边界自动调整池大小。当新任务在方法中提交时,如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。如果运行的线程多于 corePoolSize 而少于maximumPoolSize,则仅当队列满时才创建新线程。如果设置的 corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务。一般情况下这两个参数是通过构造器来设置的,但是我们可以通过提供的设置方法来处理。

  • 按需构造

默认情况下,即使核心线程最初只是在新任务需要时才创建和启动的,也可以使用方法 prestartCoreThread() 或 prestartAllCoreThreads() 对其进行动态重写。

  • 创建新线程

使用 ThreadFactory 创建新线程。如果没有另外说明,则在同一个 ThreadGroup 中一律使用 Executors.defaultThreadFactory() 创建线程,并且这些线程具有相同的 NORM_PRIORITY 优先级和非守护进程状态。通过提供不同的 ThreadFactory,可以改变线程的名称、线程组、优先级、守护进程状态,等等。如果从 newThread 返回 null 时 ThreadFactory 未能创建线程,则执行程序将继续运行,但不能执行任何任务。

  • 保持活动时间

如果池中当前有多于 corePoolSize 的线程,则这些多出的线程在空闲时间超过 keepAliveTime 时将会终止。这提供了当池处于非活动状态时减少资源消耗的方法。如果池后来变得更为活动,则可以创建新的线程。也可以使用方法 setKeepAliveTime(long, java.util.concurrent.TimeUnit) 动态地更改此参数。使用 Long.MAX_VALUE TimeUnit.NANOSECONDS 的值在关闭前有效地从以前的终止状态禁用空闲线程。

  • 排队

所有 BlockingQueue 都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互:

  1. 如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
  2. 如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
  3. 如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
  • 被拒绝的任务

当 Executor 已经关闭,并且 Executor 将有限边界用于最大线程和工作队列容量,且已经饱和时,在方法 execute(java.lang.Runnable) 中提交的新任务将被拒绝。

  • 钩子方法

此类提供 protected 可重写的 beforeExecute(java.lang.Thread, java.lang.Runnable) 和 afterExecute(java.lang.Runnable, java.lang.Throwable) 方法,这两种方法分别在执行每个任务之前和之后调用。

如果挂钩或回调方法抛出异常,则内部辅助线程将依次失败并突然终止。

  • 队列维护

方法 getQueue() 允许出于监控和调试目的而访问工作队列。强烈反对出于其他任何目的而使用此方法。remove(java.lang.Runnable) 和 purge() 这两种方法可用于在取消大量已排队任务时帮助进行存储回收。

ScheduledExecutorService

public interface ScheduledExecutorService extends ExecutorService {……}


可安排在给定的延迟后运行或定期执行的命令。

schedule 方法使用各种延迟创建任务,并返回一个可用于取消或检查执行的任务对象。scheduleAtFixedRate 和 scheduleWithFixedDelay 方法创建并执行某些在取消前一直定期运行的任务。

所有的 schedule 方法都接受相对 延迟和周期作为参数,而不是绝对的时间或日期。

ScheduledThreadPoolExecutor

public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService {……}


特殊的ThreadPoolExecutor,它可另行安排在给定的延迟后运行命令,或者定期执行命令。

一旦启用已延迟的任务就执行它,但是有关何时启用,启用后何时执行则没有任何实时保证。按照提交的先进先出 (FIFO) 顺序来启用那些被安排在同一执行时间的任务。

虽然此类继承自 ThreadPoolExecutor,但是几个继承的调整方法对此类并无作用。特别是,因为它作为一个使用 corePoolSize 线程和一个无界队列的固定大小的池,所以调整 maximumPoolSize 没有什么效果。

线程池与ThreadGroup的区别

线程组表示一个线程的集合。此外,线程组也可以包含其他线程组。线程组构成一棵树,在树中,除了初始线程组外,每个线程组都有一个父线程组。允许线程访问有关自己的线程组的信息,但是不允许它访问有关其线程组的父线程组或其他任何线程组的信息。

它们都具有线程管理和控制的功能,但是线程组更多的是考虑安全性,而线程池的出发点还是效率。