Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
点击这里查看自己实现的一个简单线程池。
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;
如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize
线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于corePoolSize时才有用,且只对corePool以外的线程有用。
keepAliveTime的时间单位
用于保存等待执行的任务的阻塞队列。当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。一般来说,我们应该尽量使用有界队列,因为使用无界队列作为工作队列会对线程池带来如下影响。
创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有的线程为守护线程。
Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”。
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
能扩展线程池的功能吗?比如在任务执行的前后做一点我们自己的业务工作?实际上,JDK 的线程池已经为我们预留的接口,在线程池核心方法中,有3 个方法是空的,就是给我们预留的。
Executor类只有一个方法:
ExecutorService类在父类的基础上增加了submit方法:
execute方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
可以通过调用线程池的shutdown()或shutdownNow()方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别:
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
性质不同的任务可以用不同规模的线程池分开处理:
CPU密集型:任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池,避免频繁地上下文切换。
+1 的作用:对于页缺失的线程(数据在硬盘上),要等待操作系统将数据从硬盘加载到内存(这时cpu被空出来了),利用这段时间可以执行另一个线程。
IO密集型:任务线程并不是一直在执行任务(阻塞),则应配置尽可能多的线程,如2*Ncpu。
此外,有Doug Lea提供的经验公式Nthreads = NCPU * UCPU * (1 + W/C)
其中:
NCPU是处理器的核的数目
UCPU是期望的CPU利用率(该值应该介于0和1之间),一般是0.9
W/C是等待时间与计算时间的比率
等待时间与计算时间我们在Linux下使用相关的vmstat命令或者top命令查看。
混合型的任务:如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。
建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。
ScheduledThreadPoolExecutor是一个使用线程池执行定时任务的类。
与Timer类比较如下:
public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit)
向定时任务线程池提交一个延时Runnable任务(仅执行一次)
public < V > ScheduledFuture< V > schedule(Callable< V > callable, long delay, TimeUnit unit);
向定时任务线程池提交一个延时的Callable任务(仅执行一次)
public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
向定时任务线程池提交一个固定时间间隔执行的任务
public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
向定时任务线程池提交一个固定延时间隔执行的任务
固定时间间隔的任务不论每次任务花费多少时间,下次任务开始执行时间从理论上讲是确定的,当然执行任务的时间不能超过执行周期。
固定延时间隔的任务是指每次执行完任务以后都延时一个固定的时间。由于操作系统调度以及每次任务执行的语句可能不同,所以每次任务执行所花费的时间是不确定的,也就导致了每次任务的执行周期存在一定的波动。
scheduleAtFixedRate中,若任务处理时长超出设置的定时频率时长,本次任务执行完才开始下次任务,下次任务已经处于超时状态,会马上开始执行。
若任务处理时长小于定时频率时长,任务执行完后,定时器等待,下次任务会在定时器等待频率时长后执行。
如下例子:
设置定时任务每60s执行一次,那么从理论上应该第一次任务在第0s开始,第二次任务在第60s开始,第三次任务在120s开始,但实际运行时第一次任务时长80s,第二次任务时长30s,第三次任务时长50s,则实际运行结果为:
第一次任务第0s开始,第80s结束;
第二次任务第80s开始,第110s结束(上次任务已超时,本次不会再等待60s,会马上开始);
第三次任务第120s开始,第170s结束.
第四次任务第180s开始…
推荐在Runnable.run()里用try-catch处理异常。
点击这里查看示例。
无继承关系,通过调用静态方法返回不同的ExecutorService。
适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。
适用于于需要保证顺序地执行各个任务,并且在任意时间点,不会有多个线程是活动的应用场景。
适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。
利用所有运行的处理器数目来创建一个工作窃取的线程池,使用forkjoin实现。
适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景。
实际上是使用了ScheduledThreadPoolExecutor类。
适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。
实际上是使用了ScheduledThreadPoolExecutor类。
CompletionService对ExecutorService进行了包装,内部维护一个保存Future对象的BlockingQueue。只有当这个Future对象状态是结束的时候,才会加入到这个Queue中,take()方法其实就是Producer-Consumer中的Consumer。它会从Queue中取出Future对象,如果Queue是空的,就会阻塞在那里,直到有完成的Future对象加入到Queue中。所以,先完成的必定先被取出。这样就减少了不必要的等待时间。
当我想要拿到线程池的返回结果时有2个办法:
点击这里查看示例。
CompletionService实际上可以看做是Executor和BlockingQueue的结合体。CompletionService在接收到要执行的任务时,通过类似BlockingQueue的take获得任务执行的结果。
CompletionService的一个实现是ExecutorCompletionService,ExecutorCompletionService把具体的计算任务交给Executor完成。
在实现上,ExecutorCompletionService(Executor)中的阻塞队列是LinkedBlockingQueue。
当提交一个任务到ExecutorCompletionService时,首先将任务包装成QueueingFuture,它是FutureTask的一个子类,然后改写FutureTask的done方法,之后把Executor执行的计算结果放入BlockingQueue中。
与ExecutorService最主要的区别在于submit的task不一定是按照加入时的顺序完成的。
【并发编程】目录:
【并发编程】之走进Java里的线程世界
【并发编程】之学会使用线程的并发工具类
【并发编程】之学会使用原子操作CAS
【并发编程】之深入理解显式锁和AQS
【并发编程】之一文彻底搞懂并发容器
【并发编程】之Java面试经常会问到的线程池,你搞清楚了吗?
【并发编程】之Java并发安全知识点总结
【并发编程】之大厂很可能会问到的JMM底层实现原理