一、线程池及线程池的优点
1、线程池的概念
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。
2、线程池的工作机制
在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程;一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
3、线程池的好处
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗
- 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))
- 提供更强大的功能,延时定时线程池
二、线程池的继承架构
JDK提供了Excutor的框架来使用线程池,它是线程池的基础。
- Executor接口:它是线程池的基础,提供了唯一的一个方法execute用来执行线程任务
- ExecutorService接口:它继承了Executor接口,提供了线程池生命周期管理的几乎所有方法,包括诸如shutdown、isshutdown、isTerminated、awaitTermination、submit、invokeAll、invokeAny等
- AbstractExecutorService类:一个提供了线程池生命周期默认实现的抽象类,并且自行新增了如newTaskFor、doInvokeAny等方法
- ThreadPoolExecutor类:这是线程池的核心类,也是我们常用来创建和管理线程池的类,我们使用Executors调用newFixedThreadPool、newSingleThreadExecutor和newCachedThreadPool、newScheduledThreadPool等方法创建的线程池,都是ThreadPoolExecutor类型的
- ScheduledExecutorService接口:赋予了线程池具备延迟和定期执行任务的能力,它提供了一些方法接口,使得任务能够按照给定的方式来延期或者周期性的执行任务
- ScheduledThreadPoolExecutor类:继承自ThreadPoolExecutor类,同时实现了ScheduledExecutorService接口,具备了线程池所有通用能力,同时增加了延时执行和周期性执行任务的能力
线程池状态
RUNNING:线程池能够接受新任务,以及对新添加的任务进行处理
SHUTDOWN:线程池不可以接受新任务,但是可以对已添加的任务进行处理
STOP:线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务
TIDYING: 当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现
TERMINATED: 线程池彻底终止的状态
线程池的核心实现类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.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
corePoolSize:核心线程数,当线程数小于该值时,线程池会优先创建新线程来执行任务,如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程,除非设置了allowCoreThreadTimeOut,否则核心线程将持续保留在线程池中即时没有新的任务提交过来。
maximumPoolSize:最大线程数,线程池所允许的最大线程个数,当核心线程数满了,队列也满了的时候,已创建的线程数小于最大线程数,则线程池会创建新的线程来执行任务,如果队列是无界队列该参数可忽略。
keepAliveTime:空闲线程存活时间,当线程数量大于核心线程数时,这是多余空闲线程在终止之前等待新任务的最长时间。
unit:keepAliveTime数值的时间单位。
workQueue:任务队列,用于缓存未执行的任务,队列一直会持有任务直到有线程开始执行它。
threadFactory:线程工厂,可以通过工厂创建更具识别性质的线程,如线程名字等。用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)
handler:拒绝策略,当线程池和队列都处于饱和时就使用拒绝策略来处理新任务。
线程池中线程的使用和创建规则
在Java线程池的实现逻辑中,线程池所能创建的线程数量受限于corePoolSize和maximumPoolSize两个参数值,线程的创建时机则和corePoolSize和workQueue两个参数相关,当线程数量和队列长度都已达到饱和时,则介入拒绝策略来处理新的任务了
1、当来了新任务,如果线程池中空闲线程数量小于corePoolSize,则直接拿线程池中新的线程来处理任务;
2、如果线程池正在运行的线程数量大于等于corePoolSize,而此时workQueue队列未满,则将此任务缓存到队列中;
3、如果线程池正在运行的线程数量大于等于corePoolSize,且workQueue队列已满,但现在的线程数量还小于maximumPoolSize,则创建新的线程来执行任务。
4、如果线程数量已经大于maximumPoolSize且workQueue队列也已经满了,则使用拒绝策略来处理该任务,默认的拒绝策略就是抛出异常(AbortPolicy)。
常用的几种线程池
一般情况下我们都不直接用ThreadPoolExecutor类来创建线程池,而是通过Executors工具类去构建,通过Executors工具类我们可以构造5种不同的线程池
newFixedThreadPool(int nThreads):
创建固定线程数的线程池,corePoolSize和maximumPoolSize是相等的,默认情况下,线程池中的空闲线程不会被回收的
newCachedThreadPool:
创建线程数量不定的线程池,线程数量随任务量变动,一旦来了新的任务,如果线程池中没有空闲线程则立马创建新的线程来执行任务,空闲线程存活时间60秒,过后就被回收了,可见这个线程池弹性很高
newSingleThreadExecutor:
创建线程数量为1的线程池,等价于newFixedThreadPool(1)所构造的线程池
newScheduledThreadPool(int corePoolSize):
创建核心线程数为corePoolSize,可执行定时任务的线程池
newSingleThreadScheduledExecutor:
等价于newScheduledThreadPool(1)
阻塞队列
因为线程若是无限制的创建,可能会导致内存占用过多而导致OOM,并且会导致CPU过度切换
当线程数量大于corePoolSize而当前workQueue还没有满时,就需要将任务放置到队列中。JDK提供了几种类型的队列容器,每种类型都具各自特点,可以根据实际场景和需要自行配置到线程池中
ArrayBlockingQueue:
一个用数组实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁
LinkedBlockingQueue:
一个由链表结构组成的有界队列,此队列按照先进先出(FIFO)的原则对元素进行排序。此队列默认长度为Integer.MAX_VALUE,所以默认创建该队列会有容量危险
SynchronousQueue:
同步队列,该队列不存储元素,每个插入操作必须等待另一个线程调用移除操作,否则插入操作会一直被阻塞,Executors.newCachedThreadPool()使用了这个队列
PriorityBlockingQueue:
优先级队列,具有优先级的无限阻塞队列
拒绝策略
拒绝策略(RejectedExecutionHandler)也称饱和策略,当线程数量和队列都达到饱和时,就采用饱和策略来处理新提交过来的任务,默认情况下采用的策略是抛出异常(AbortPolicy),表示无法处理直接抛出异常,其实JDK提供了四种策略,也很好记,拒绝策略无非就是抛异常、执行或者丢弃任务,其中丢弃任务就分为丢弃自己或者丢弃队列中最老的任务,下面简要说明一下:
AbortPolicy:丢弃新任务,并抛出 RejectedExecutionException
DiscardPolicy:不做任何操作,直接丢弃新任务
DiscardOldestPolicy:丢弃队列队首(最老)的元素,并执行新任务
CallerRunsPolicy:由当前调用线程来执行新任务
使用技巧
- 任务的性质:CPU密集型任务,IO密集型任务和混合型任务
- 任务的优先级:高,中和低
- 任务的执行时间:长,中和短
- 任务的依赖性:是否依赖其他系统资源,如数据库连接
CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池,以减少线程切换带来的性能开销。(即使当计算(CPU)密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保 CPU 的时钟周期不会被浪费)
IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。(Nthread=Ncpu*Ucpu*(1+W/C))
混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
public class Test { public static void main(String[] args) throws InterruptedException{ //自定义一个线程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(10),new NamedThreadFactory("线程程")); for(int i=0;i<15;i++){ threadPoolExecutor.submit(()->{ threadPoolStatus(threadPoolExecutor,"创建任务"); try{ TimeUnit.SECONDS.sleep(10); }catch (Exception e){ e.printStackTrace(); } }); } threadPoolStatus(threadPoolExecutor,"改变之前"); threadPoolExecutor.setCorePoolSize(10); threadPoolExecutor.setMaximumPoolSize(10); threadPoolStatus(threadPoolExecutor,"改变之后"); Thread.currentThread().join(); // threadPoolExecutor.shutdown(); } private static void threadPoolStatus(ThreadPoolExecutor executor,String name){ LinkedBlockingQueue queue = (LinkedBlockingQueue)executor.getQueue(); System.out.println(Thread.currentThread().getName()+"-"+name+"-:"+ "核心线程数:"+executor.getCorePoolSize()+ "活动线程数:"+executor.getActiveCount()+ "最大线程数:"+executor.getMaximumPoolSize()+ "线程活跃度:"+divide(executor.getActiveCount(),executor.getMaximumPoolSize())+ "任务完成数:"+executor.getCompletedTaskCount()+ "队列大小:"+(queue.size()+queue.remainingCapacity())+ "当前排队线程数:"+queue.size()+ "队列剩余大小:"+queue.remainingCapacity()+ "队列实用度:"+divide(queue.size(),queue.size()+queue.remainingCapacity())); } private static String divide(int num1,int num2){ return String.format("%1.2f%%",Double.parseDouble(num1+"")/Double.parseDouble(num2+"")*100); } }
动态化配置核心线程数和最大线程数的实例
在运行期线程池使用方调用此方法设置corePoolSize之后,线程池会直接覆盖原来的corePoolSize值,并且基于当前值和原始值的比较结果采取不同的处理策略。
对于当前值小于当前工作线程数的情况,说明有多余的worker线程,此时会向当前idle的worker线程发起中断请求以实现回收,多余的worker在下次idel的时候也会被回收;
对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务
1.首先是参数合法性校验。
2.然后用传递进来的值,覆盖原来的值。
3.判断工作线程是否是大于最大线程数,如果大于,则对空闲线程发起中断请求。
设置核心线程数的时候,同时设置最大线程数即可。其实可以把二者设置为相同的值;当 allowCoreThreadTimeOut 参数设置为 true 的时候,核心线程在空闲了 keepAliveTime 的时间后也会被回收的,相当于线程池自动给你动态修改了。
动态化队列长度
队列的长安capacity是被final修饰的,所以我们需要自定义一个队列,把capacity进行修改就可以了,可以把LinkedBlockingQueue复制一份,然后把 Capacity 参数的 final 修饰符去掉,并提供其对应的 get/set 方法让我们对capacity进行修改,创建线程池的时候队列就用我们自定义的线程池,使用时获取自定义的线程池然后设置想要设置的长度即可。
三、Executors几种常用线程池
如果确定你的并发量有限,并且每个线程占用的内存大小有限,你可以使用Executors来建立线程池
如果你的并发量没有办法控制,并且每个线程占用的内存大小无法确定较小,那么你需要使用ThreadPoolExecutor的方式来创建线程
1、newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
2、newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
3、newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
适用于需要保证顺序执行各个任务。
4、newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求,适用于执行延时或者周期性任务。
newCachedThreadPool
初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;newCachedThreadPool在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销;所以,使用该线程池时,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题
public class Test { public static void main(String[] args) { ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for(int i=1;i<10;i++){ try{ Thread.sleep(1000); }catch (Exception e){ e.printStackTrace(); } cachedThreadPool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "正在被执行"); } }); } } }
线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程
newFixedThreadPool
初始化一个指定线程数的线程池,其中corePoolSize == maximumPoolSize,使用LinkedBlockingQuene作为阻塞队列,不过当线程池没有可执行任务时,也不会释放线程
public class Test { public static void main(String[] args) { ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); for(int i=1;i<10;i++){ try{ Thread.sleep(1000); }catch (Exception e){ e.printStackTrace(); } fixedThreadPool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "正在被执行"); } }); } } }
因为线程池大小为3,每个任务输出打印结果后sleep 2秒,所以每两秒打印3个结果。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()
newSingleThreadExecutor
初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行,内部使用LinkedBlockingQueue作为阻塞队列
public class Test { public static void main(String[] args) { ExecutorService singleThreadPool = Executors.newSingleThreadExecutor(); for(int i=1;i<10;i++){ final int index = i; singleThreadPool.execute(new Runnable() { @Override public void run() { try{ System.out.println(Thread.currentThread().getName()+"--"+index); Thread.sleep(3000); }catch (Exception e){ e.printStackTrace(); } } }); } singleThreadPool.shutdown(); } }
一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
newScheduledThreadPool
初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据
public class Test { public static void main(String[] args) { ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5); scheduledExecutorService.schedule(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"每3秒执行一次"); } }, 3, TimeUnit.SECONDS); } }
一个定长线程池,支持定时及周期性任务执行
四、线程池不允许使用Executors去创建
阿里巴巴java开发手册有这么一句话:“线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险”。
Executors创建线程池的方法有三种:
- Executors.newFixedThreadPool--->创建固定长度的线程池
- Executors.newCachedThreadPool-->创建可缓存的线程池
- Executors.newSingleThreadExecutor-->创建单线程的线程池
FixedThreadPool
corePoolSize => x,核心线程池的数量为x maximumPoolSize => x,可以创建x个非核心线程 keepAliveTime => 0L unit => 秒 workQueue => LinkedBlockingQueue 它和SingleThreadExecutor类似,唯一的区别就是核心线程数不同,并且由于使用的是LinkedBlockingQueue,在资源有限的时候容易引起OOM异常
CachedThreadPool
corePoolSize => 0,核心线程池的数量为0 maximumPoolSize => Integer.MAX_VALUE,可以认为最大线程数是无限的 keepAliveTime => 60L unit => 秒 workQueue => SynchronousQueue 当一个任务提交时,corePoolSize为0不创建核心线程,SynchronousQueue是一个不存储元素的队列,可以理解为队里永远是满的,因此最终会创建非核心线程来执行任务。 对于非核心线程空闲60s时将被回收。因为Integer.MAX_VALUE非常大,可以认为是可以无限创建线程的,在资源有限的情况下容易引起OOM异常
SingleThreadExecutor
corePoolSize => 1,核心线程池的数量为1 maximumPoolSize => 1,只可以创建一个非核心线程 keepAliveTime => 0L unit => 秒 workQueue => LinkedBlockingQueue 当一个任务提交时,首先会创建一个核心线程来执行任务,如果超过核心线程的数量,将会放入队列中,因为LinkedBlockingQueue是长度为Integer.MAX_VALUE的队列,可以认为是无界队列, 因此往队列中可以插入无限多的任务,在资源有限的时候容易引起OOM异常,同时因为无界队列,maximumPoolSize和keepAliveTime参数将无效,压根就不会创建非核心线程
这就是为什么禁止使用Executors去创建线程池,而是推荐自己去创建ThreadPoolExecutor的原因
线程池推荐创建方式
首先引入:com.google.guava包
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() .setNameFormat("demo-pool-%d").build(); //Common Thread Pool ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy()); pool.execute(()-> System.out.println(Thread.currentThread().getName())); pool.shutdown();//gracefully shutdown