线程池的优势:
-
线程复用:线程池中的线程是可以复用的,省去了创建、销毁线程的开销,提高了资源利用率;
-
合理利用资源:通过调整线程池大小,让所有处理器尽量保持忙碌,又能防止过多线程产生过多竞争浪费资源;
线程池实现原理
提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
举例:
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(5);
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, queue);
运行原理:
通过queue.size()的方法来获取工作队列中的任务数;
刚开始都是在创建新的线程,达到核心线程数量5个后,新的任务进来后不再创建新的线程,而是将任务加入工作队列,任务队列到达上线5个后,新的任务又会创建新的普通线程,直到达到线程池最大的线程数量10个,后面的任务则根据配置的饱和策略来处理。
RejectedExecutionHandler:饱和策略
当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。
JAVA提供了4种策略:
◾AbortPolicy:直接抛出异常
◾CallerRunsPolicy:只用调用所在的线程运行任务
◾DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
◾DiscardPolicy:不处理,丢弃掉。
Executor接口
在JAVA中,任务执行的主要抽象不是Thread,而是Executor。Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者。
所谓Executor框架,其实就是定义了一个接口,我们常用的线程池ThreadPoolExecutor就是对这个接口的一种实现。
常用的线程池主要是ThreadPoolExecutor 和 ScheduledThreadPoolExecutor(定时任务线程池,继承ThreadPoolExecutor)。
Executor框架的两级调度模型
在HotSpot VM的模型中,JAVA线程被一对一映射为本地操作系统线程。JAVA线程启动时会创建一个本地操作系统线程,当JAVA线程终止时,对应的操作系统线程也被销毁回收,而操作系统会调度所有线程并将它们分配给可用的CPU。在上层,JAVA程序会将应用分解为多个任务,然后使用应用级的调度器(Executor)将这些任务映射成固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。
1、从类图上看,Executor接口是异步任务执行框架的基础,该框架能够支持多种不同类型的任务执行策略。
public interface Executor {
void execute(Runnable command);
}
Executor接口就提供了一个执行方法,任务是Runnbale类型,不支持Callable类型。
2、ExecutorService接口实现了Executor接口,主要提供了关闭线程池和submit方法:
public interface ExecutorService extends Executor {
List<Runnable> shutdownNow();
boolean isTerminated();
<T> Future<T> submit(Callable<T> task);
}
另外该接口有两个重要的实现类:ThreadPoolExecutor与ScheduledThreadPoolExecutor。
其中ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务;而ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行任务,或者定期执行命令。
Executors与常用线程池
之前使用ThreadPoolExecutor来通过给定不同的参数从而创建自己所需的线程池,但是在后面的工作中不建议这种方式,推荐使用Exectuors工厂方法来创建线程池。
Executors是一个Executor的工厂,有很多定义好的工厂方法,可以帮助懒惰的开发者快速创建一个线程池。
下面是几个常用的工厂方法:
-
newFixedThreadPool 固定长度线程池,每次提交任务都会创建一个新线程,直到线程数量达到指定阈值则不再创建新的;
-
newCachedThreadPool 可缓存线程池(无界线程池),没有工作队列,任务进来就执行,线程数量不够就创建,与前面两个的区别是:空闲的线程会被回收掉,空闲的时间是60s。这个适用于执行很多短期异步的小程序或者负载较轻的服务器。
-
newSingleThreadExecutor 只有一个线程的线程池;
-
newScheduledThreadPool 可以延时或者定时执行任务的线程池。
线程池构造参数
-
int corePoolSize : 核心线程数,有新任务来时,如果当前线程小于核心线程,则新建一个线程来执行该任务
-
int maximumPoolSize : 最大线程数,线程池最多拥有的线程数
-
long keepAliveTime : 空闲线程存活时间
-
TimeUnit unit : 空闲线程存活时间的单位
-
BlockingQueue
workQueue : 存放待执行任务的阻塞队列,新任务来时,若当前线程数>=最大核心线程数,则放到这个队列
-
ThreadFactory threadFactory : 创建新线程的工厂,一般用来给线程取个名字方便排查问题
-
RejectedExecutionHandler handler : 任务被拒绝后的处理器,默认的处理器会直接抛出异常,建议重新实现
有返回值的提交方式
ThreadPoolExecutor.execute()方法是没有返回值的;
可以使用ThreadPoolExecutor.submit()来提交任务,这个方法会返回一个Future对象,通过这个对象可以知道任务何时被执行完。
submit原理:
submit时用一个FutureTask把用户提交的Callable包装起来,再把FutureTask提交给线程池执行,FutureTask.run运行时会执行Callable中的业务代码,并且过程中FutureTask会维护一个状态标识,根据状态标识,可以知道任务是否执行完成,也可以阻塞到状态为完成获取返回值。
Callable、Future、FutureTash详解
Callable与Future是在JAVA的后续版本中引入进来的,Callable类似于Runnable接口,实现Callable接口的类与实现Runnable的类都是可以被线程执行的任务。
三者之间的关系:
◾Callable是Runnable封装的异步运算任务。
◾Future用来保存Callable异步运算的结果,主要是判断任务是否完成、中断任务、获取任务执行结果。
◾FutureTask封装Future的实体类
FutureTask不仅实现了Future接口,还实现了Runnable接口,所以不仅可以将FutureTask当成一个任务交给Executor来执行,还可以通过Thread来创建一个线程。
关闭线程池
为什么需要关闭线程池?
-
如果线程池里的线程一直存活,而且这些线程又不是守护线程,那么会导致虚拟机无法正常退出;
-
如果直接粗暴地结束应用,线程池中的任务可能没执行完,业务将处于未知状态;
-
线程中有些该释放的资源没有被释放。
怎么关闭线程池?
-
shutdown 停止接收新任务(继续提交会被拒绝,执行拒绝策略),但已提交的任务会继续执行,全部完成后线程池彻底关闭;
-
shutdownNow 立即停止线程池,并尝试终止正在进行的线程(通过中断),返回没执行的任务集合;
-
awaitTermination 阻塞当前线程,直到全部任务执行完,或者等待超时,或者被中断。
由于shutdownNow的终止线程是通过中断,这个方式并不能保证线程会提前停止。(关于中断: 如何处理线程中断)
一般先调用shutdown让线程池停止接客,然后调用awaitTermination等待正在工作的线程完事。
其他情况
1、举个例子:如果设置了核心线程 < 最大线程数不等(一般都这么设置),但是又设置了一个很大的阻塞队列,那么很可能只有几个核心线程在工作,普通线程一直没机会被创建,因为核心线程满了会优先放到队列里,而不是创建普通线程。
2、如有一种场景中,方法A返回一个数据需要10s,A方法后面的代码运行需要20s,但是这20s的执行过程中,只有后面10s依赖于方法A执行的结果。如果与以往一样采用同步的方式,势必会有10s的时间被浪费,如果采用前面两种组合,则效率会提高:
◾先把A方法的内容放到Callable实现类的call()方法中
◾在主线程中通过线程池执行A任务
◾执行后面方法中10秒不依赖方法A运行结果的代码
◾获取方法A的运行结果,执行后面方法中10秒依赖方法A运行结果的代码
这样代码执行效率一下子就提高了,程序不必卡在A方法处。
参考如下: