spring的线程池技术使用的是ThreadPoolTaskExecutor,当我们点进ThreadPoolTaskExecutor这个类的时候,可以看见里面是对于JDK中的ThreadPoolExecutor的封装:
1、为什么要使用线程池?
创建和销毁线程的花销是很大的,处理时间可能比处理业务的时间还长。
频繁创建和销毁线程,可能导致系统资源不足
2、线程池有什么作用?
(1)提高效率:创建好一定数量的线程放在池中,需要的时候就去池中拿一个,比需要的时候再去创建一个线程对象要快的多;
(2)便于管理:程序启动时,管理代码去创建100个线程,有请求的时候就分配一个线程去工作,若有101个请求,则第101个请求 排队等候,避免频繁创建线程,导致系统崩溃。
3、常见线程池创建及其使用场景
java里面的线程池的顶级接口是Executor,Executor并不是一个线程池,而只是一个执行线程的工具,而真正的线程池是ExecutorService
(1)newCachedThreadPool创建一个可缓存线程池:线程数量不定,最大线程数为Integer.MAX_VALUE。空闲线程超时60秒,会被回收。若无可回收,会新建线程。若线程可用的话,调用execute方法将重用以前构造的线程。此类线程池比较适合执行大量的耗时较少的任务。整个线程池都处于闲置状态时,线程池中的线程都会超时被停止。
实例代码:
public Class CachedThreadPoolTest{
public static void main(String[] args) Throws InterruptedException{
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for(int i=0;i<7;i++){
final int index = i;
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable(){
@Override
public void run(){
sysout(“第”+index+"个线程"+Thread.currentThread().getName());
}
});
}
}
}
创建可缓存线程池的具体实现:
public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor( 0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}
输出结果是:
第0个线程pool-1-thread-1
第1个线程pool-1-thread-1
第2个线程pool-1-thread-1
第3个线程pool-1-thread-1
第4个线程pool-1-thread-1
第5个线程pool-1-thread-1
第6个线程pool-1-thread-1
从结果可以看出,执行第二个任务的时候第一个任务已经完成,会复用执行第一个任务的线程,不用每次都新建线程
(2)newFixedTheadPool创建一个定长线程池:指定线程数量,每当提交一个任务就创建一个工作线程。当线程处于空闲状态,不会被回收,除非线程池被关闭了。若工作线程数量达到指定的初始化线程数大小,则将提交的任务存入到池队列中,当有线程可用时,任务开始执行。由于newFixedTheadPool只有核心线程并且这些核心线程不会被回收,这样它更快响应外界请求。
实例代码:
public Class FixedThreadPoolTest{
public static void main(String[] args) Throws InterruptedException{
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);//设置最大线程数5个
for(int i=0;i<7;i++){
final int index = i;
fixedThreadPool .execute(new Runnable(){
@Override
public void run(){
sysout(“时间是:”+System.currentTimeMillis()+"第"+index+"个线程"+Thread.currentThread().getName());
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
});
}
}
}
创建定长线程池的具体实现:
public static ExecutorService newFixedThreadPool( int nThreads){
return new ThreadPoolExecutor( nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
输出结果是:
时间是:1533382000965第0个线程pool-1-Thread-1
时间是:1533382000966第4个线程pool-1-Thread-5
时间是:1533382000965第3个线程pool-1-Thread-4
时间是:1533382000965第1个线程pool-1-Thread-2
时间是:1533382000965第2个线程pool-1-Thread-3
时间是:1533382002966第5个线程pool-1-Thread-3
时间是:1533382002966第6个线程pool-1-Thread-5
同一时间5个线程同时执行任务,故前五个打印时间是相同的,线程执行顺序随机。等待两秒后,再执行后两个任务
(3)newScheduledThreadPool创建一个定长线程池:核心线程数量固定而非核心线程数是没有限制的,且当非核心线程空闲时会被立即回收,它可安排给定延迟后运行命令或者定期地执行。此类线程池主要用于执行定时任务和具有固定周期的重复任务。
延迟执行代码示例:
public Class ScheduledThreadPoolTest{
public static void main(String[] args) Throws InterruptedException{
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);//设置池中核心线程数量是2
sysout("现在的时间是:"+System.currentTimeMillis());
scheduledThreadPool .schedule(new Runnable(){
@Override
public void run(){
sysout("现在的时间是:"+System.currentTimeMillis());
}
}, 4, TimeUnit.SECONDS);//设置延迟4秒执行
}
}
执行结果是:
现在的时间是:1533384023005
现在的时间是:1533384027007
忽略误差,实际结果确实延迟了4秒执行
定期执行代码示例:
public Class ScheduledThreadPoolTest{
public static void main(String[] args) Throws InterruptedException{
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);//设置池中核心线程数量是2
sysout("现在的时间是:"+System.currentTimeMillis());
scheduledThreadPool .scheduleAtFixedRate(new Runnable(){
@Override
public void run(){
sysout("现在的时间是:"+System.currentTimeMillis());
}
}, 2, 3, TimeUnit.SECONDS);//设置延迟2秒后每3秒执行一次
}
}
创建定时及周期性执行任务的定长线程池的具体实现:
public static ScheduledExecutorService newScheduledThreadPool( int corePoolSize){
return new ScheduledThreadPoolExecutor( corePoolSize );
}
执行结果是:
现在的时间是:1533384668068
现在的时间是:1533384070070
现在的时间是:1533384073070
现在的时间是:1533384076070
现在的时间是:1533384079070
确实是延迟2秒后每隔3秒执行一次,程序不退出就会一直执行打印下去
(4)newSingleThreadExecutor创建一个单线程化的线程池:只有一个核心线程,以无界队列(无界队列在最末尾有解释)方式来执行该线程,任务间不需要处理线程同步的问题,它确保所有的任务都在同一个线程中按顺序执行,且在任意给定时间不会有多个线程是活动的
实例代码:
public Class SingleThreadPoolTest{
public static void main(String[] args) Throws InterruptedException{
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
for(int i=0;i<7;i++){
final int index =i;
singleThreadPool.execute(new Runnable(){
@Override
public void run(){
sysout("现在的时间是:"+System.currentTimeMillis()+"第"+index+“个线程”);
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
});
}
}
}
创建单线程池的具体实现:
public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegatedExecutorService( new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
}
执行结果是:
现在的时间是:1533385933444第0个线程
现在的时间是:1533385935445第1个线程
现在的时间是:1533385937445第2个线程
现在的时间是:1533385939445第3个线程
现在的时间是:1533385941445第4个线程
现在的时间是:1533385943445第5个线程
现在的时间是:1533385945445第6个线程
单个线程顺序执行上面7个线程(可以将线程名打印出来,可以发现是同一个线程)
4、使用Executors各个方法的弊端:
newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至out of memory;
newCachedThreadPool和newScheduledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至out of memory;
5、线程池的四种工作队列:
(1)(常用)LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO先进先出排序元素,吞吐量高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列;
(2)(常用)SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列;
(3)(不常用)ArrayBlockingQueue:一个基于数组结构的有界阻塞队列,此队列按FIFO先进先出原则对元素进行排序;
(4)(不常用)PriorityBlockingQueue:一个具有优先级的无限阻塞队列;
6、线程池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();
}
corePoolSize :核心线程数大小。默认情况下创建线程池后,池中是没有任何线程的(线程数为0),而是等待有任务到来才去创建一个线程执行任务。若调用了prestartAllCoreThreads()或者prestartCoreThread()方法,在没有任务到来之前就预创建corePoolSize个线程或者一个线程。当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列中去;
maximumPoolSize:线程池最大线程数,表示线程池最多能创建多少个线程;
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。当线程池中的线程数大于corePoolSize时,keepAliveTime才会生效使一个线程空闲时间达到它时终止,直到线程池中的线程数不大于corePoolSize。若调用了allowCoreThreadTimeOut(boolean)方法,在线程池中额线程数不大于corePoolSize时,keepAliveTime参数也会起作用,知道线程池中线程数为0;
unit:参数keepAliveTime的时间单位,7种取值是TimeUnit类中的7中静态属性:
TimeUnit.DAYS / TimeUnit.HOURS / TimeUnit.MINUTES / TimeUnit.SECONDS / TimeUnit.MILLISECONDS / TimeUnit.MICROSECONDS / TimeUnit.NANOSECONDS纳秒
workQueue:一个阻塞队列,用来存储等待执行的任务,种类在上一部分5中有详解。线程池的排队策略与blockingQueue有关;
threadFactory:用于设置创建线程的工厂,可以通过线程工厂给线程设置deamon和优先级等
handler:表示当拒绝处理任务时的策略,有四种取值:ThreadPoolExecutor.AbortPolicy(直接丢弃任务并抛出RejectedExecutionException异常)、ThreadPoolExecutor.CallerRunsPolicy(只用调用者所在线程来运行任务)、ThreadPoolExecutor.DiscardOldestPolicy(丢弃队列里最前面的一个任务,并执行当前任务)、ThreadPoolExecutor.DiscardPolicy(丢弃任务但不抛出异常)
还可根据应用场景需要来实现RejectedExeutionHandler接口自定义策略;
7、有界队列和无界队列
有界队列
(1)初始的poolSize< corePoolSize,提交的runnable任务,会直接作为new一个thread的参数,立马执行;
(2)当提交的任务数超过了corePoolSize,会将当前的runnable提交到一个block queue中;
(3)有界队列满了之后,如果poolSize < maximumPoolSize时,会尝试new一个thread的进行救急处理,立马执行对应的runnable任务;
(4)如果3中也无法处理了,就会走到第四步执行reject操作;
无界队列:
除非系统资源耗尽,否则无界队列不存在任务入队失败的情况。当有新任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略。
合理配置线程池大小:https://www.zhihu.com/question/38128980
8、如果提交任务时,线程池队列已满,会发生什么?
如果一个任务不能被调度执行,那么ThreadPoolExecutor的submit()方法会抛出一个RejectedExecutionException异常;
9、线程池的submit()和execute()方法有什么区别?
两个方法都可以向线程池提交任务。
execute方法是void型的,定义在Executor接口中;
submit方法可以返回持有计算结果的future对象,定义在ExecutorService接口中,扩展了Executor接口;
ThreadPoolExecutor和ScheduledThreadPoolExecutor线程池都有这些方法。