10065---【Java多线程】线程池的工作原理详解(上)

来源:互联网 发布:淘宝网店从哪里注册 编辑:程序博客网 时间:2024/06/07 00:32

原文

为什么使用线程池

线程池为线程生命周期开销问题和资源不足问题提供了解决方案,因为线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池对线程进行统一分配、调优和监控;

多线程技术可以解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力;

什么时候使用线程池

需要大量的线程来完成任务,且完成任务的时间比较短,如WEB服务器完成网页请求这样的任务。因为单个任务小,而任务数量巨大,比如一个热门网站的点击次数。 但对于长时间的任务,比如一个ftp连接请求,线程池的优点就不明显了。因为ftp会话时间相对于线程的创建时间长多了。

对性能要求苛刻的应用,比如要求服务器迅速相应客户请求。

接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限。

使用线程池的好处

降低资源消耗;
提高响应速度;
提高线程的可管理性


线程池类之间的关系

UML图


分析:我介绍一下常用的几个类或接口

ExecutorService :线程池接口,继承于Executor接口,多了Submit、shutdown等方法
ScheduledExecutorService:功能和Timer类似,解决那些需要任务重复执行的问题,负责线程的调度
ThreadPoolExecutor :线程池的实现类
ScheduledThreadPoolExecutor:继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。
Executors :一个工具类,提供几个创建线程池的方法
Future 接口:它和FutureTask类(实现了Future接口)都用来表示异步计算的结果。

ThreadPoolExecutor

1.定义

一个 ExecutorService,它使用可能的几个池线程之一执行每个提交的任务,通常使用 Executors 工厂方法配置。

2.线程池的处理流程


3.ThreadPoolExecutor的构造方法

public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue<Runnable> workQueue,                              ThreadFactory threadFactory,                              RejectedExecutionHandler handler) {        if (corePoolSize < 0 ||            maximumPoolSize <= 0 ||            maximumPoolSize < corePoolSize ||            keepAliveTime < 0)            throw new IllegalArgumentException();        if (workQueue == null || threadFactory == null || handler == null)            throw new NullPointerException();        this.corePoolSize = corePoolSize;        this.maximumPoolSize = maximumPoolSize;        this.workQueue = workQueue;        this.keepAliveTime = unit.toNanos(keepAliveTime);        this.threadFactory = threadFactory;        this.handler = handler;    }

4.构造方法参数详解

(1)corePoolSize :线程池中的核心线程数
当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

(2)maximumPoolSize :线程池中允许的最大线程数
如果当前阻塞队列满了,判断是否达到最大线程数,若没有,则创建新的线程执行任务,否则进行拒绝策略。但如果使用了无界的任务队列这个参数就没用了。

(3)keepAliveTime :线程空闲时的存活时间
当线程没有任务执行时,继续存活的时间;默认情况下,该参数只在线程数大于corePoolSize时才有用。

(4)unit :keepAliveTime的单位

(5)workQueue :用来保存等待被执行的任务的阻塞队列


任务必须实现Runable接口,一共4有个阻塞队列:

ArrayBlockingQueue:基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。

LinkedBlockingQuene:基于链表结构的无界阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;

SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;

PriorityBlockingQuene:具有优先级的无界阻塞队列;

(6)threadFactory :创建线程的工厂

可以选择DefaultThreadFactory,将创建一个同线程组且默认优先级的线程,也可以选择PrivilegedThreadFactory,使用访问权限创建一个权限控制的线程。但默认采用DefaultThreadFactory,当然也可以创建自定义线程工厂给每个新建的线程设置一个具有识别度的线程名。

(7)handler
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:

AbortPolicy:直接抛出RejectedExecutionException异常,默认策略;

CallerRunsPolicy:如果发现线程池还在运行,就直接运行这个线程,即用调用者所在的线程来执行任务;

DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

DiscardPolicy:直接丢弃任务;

当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

5.提交任务的方法


(1)submit()方法 :用于提交需要返回值的任务
线程池会返回一个Future类型的对象,通过这个 Future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,但get()方 法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线 程一段时间后立即返回,这时候有可能任务没有执行完。

(2)execute()方法 :用于提交不需要返回值的任务,
无法判断任务是否被线程池执行成功,线程池在执行excute方法时,会出现以下几种情况:

① 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(需要获得全局锁);
②如果运行的线程等于或多于corePoolSize ,则将任务加入BlockingQueue;
③如果无法将任务加入BlockingQueue(队列已满并且正在运行的线程数量小于 maximumPoolSize),则创建新的线程来处理任务(需要获得全局锁)
④如果创建新线程将使当前运行的线程超出maxiumPoolSize(队列已满并且正在运行的线程数量大于或等于 maximumPoolSize),任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法(线程池会抛出异常,告诉调用者”我不能再接受任务了”);

以上四种情况图解: 


线程池采取上述的流程进行设计是为了减少获取全局锁的次数。在线程池完成预热(当前运行的线程数大于或等于corePoolSize)之后,几乎所有的excute方法调用都执行步骤②,还有两种情况如下:

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


6.关闭线程池的方法

(1)关闭线程池方法的原理
遍历线 程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务 可能永远无法终止。
(2)shutdown方法(常用) :将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
(3)shutdownNow方法:首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

(4)可以调用isShutdown方法来判断线程池是否关闭

ScheduledThreadPoolExecutor

1.引入

自jdk1.5开始,Java开始提供ScheduledThreadPoolExecutor类来支持周期性任务的调度,在这之前,这些工作需要依靠Timer/TimerTask或者其它第三方工具来完成。但Timer有着不少缺陷,如Timer是单线程模式,调度多个周期性任务时,如果某个任务耗时较久就会影响其它任务的调度;如果某个任务出现异常而没有被catch则可能导致唯一的线程死掉而所有任务都不会再被调度。ScheduledThreadPoolExecutor解决了很多Timer存在的缺陷。

2.类的继承关系

注:绿色虚线表示实现的接口,黄色实线表示继承的类,绿色实线表示接口间的继承。

3.执行周期任务的步骤

(1)图解: