1.为什么用线程池,线程池的优势
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
他的主要特点为:线程复用,控制最大并发数,线程管理。
- 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁的消耗。
- 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调用和监控。
2.架构说明
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
3.Executors常见的创建线程池的方式
- 1.Executors.newFixedThreadPool(int)
执行一个长期的任务,性能好很多。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
主要特点如下:
1.创建一个定长线程池,可控制线程的最大并发数,超出的线程会在队列中等待.
2.newFixedThreadPool创建的线程池corePoolSize和MaxmumPoolSize是 相等的,它使用的的LinkedBlockingQueue
- 2.Executors.newSingleThreadExecutor()
一个任务一个线程执行的任务场景
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
主要特点如下:
1.创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务都按照指定顺序执行.
2.newSingleThreadExecutor将corePoolSize和MaxmumPoolSize都设置为1,它使用的的LinkedBlockingQueue
3.Executors.newCachedThreadPool()
- 3.Executors.newCachedThreadPool()
适应:执行很多短周期异步的小程序或者负载较轻的服务器
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
主要特点如下:
1.创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则创建新线程.
2.newCachedThreadPool将corePoolSize设置为0MaxmumPoolSize设置为Integer.MAX_VALUE,它使用的是SynchronousQUeue,也就是说来了任务就创建线程运行,如果线程空闲超过60秒,就销毁线程
4.线程池底层实现(ThreadPoolExecutor)
5.线程池7个重要参数介绍
- 1.corePoolSize:线程池中的常驻核心线程数。
- 1.在创建了线程池后,当有请求任务来之后,就会安排池中得线程去执行请求任务,近似理解为当日当值线程。
- 2.当线程池中的线程数目达到corePoolSize后,就会把到达的任务放入到缓存队列中。
- 2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值大于等于1。
- 3.keepAliveTime:多余的空闲线程存活时间,当空闲时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止。(默认情况下:只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用)。
- 4.unit:keepAliveTime的单位。
- 5.workQueue:任务队列,被提交但尚未被执行的任务。
- 6.threadFactory:表示生产线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可。
- 7.handler:拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示数(maximumPoolSize)时,如何来拒绝。
6.线程池底层工作原理
- 1.在创建了线程池后,等待提交过来的任务请求。
- 2.当调用execute()方法添加一个请求任务时,线程池会做如下判断:
- 2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
- 2.3 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池启动饱和拒绝策略来执行;
- 3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
7.线程池的拒绝策略
- 1.拒绝策略是什么?
等待队列也已经排满了,再也塞不下新的任务了。同时,线程池的max也到达了,无法接续为新任务服务。这时我们需要拒绝策略机制合理的处理这个问题.。 - 2.JDK内置的拒绝策略
- 2.1 AbortPolicy 抛出异常。
不执行此任务,而且直接抛出一个运行时异常 RejectedExecutionException,为java线程池默认的阻塞策略。切记会中断调用者的处理过程,因此需要try catch,否则程序会直接退出。 - 2.2 DiscardPolicy 直接静悄悄的丢弃这个任务,不触发任何动作。这个策略基本不会使用。
- 2.3 DiscardOldestPolicy 丢弃队列最前面(最旧)的任务,然后重新尝试执行任务(重复此过程)。
- 2.4 CallerRunsPolicy 由调用线程处理该任务。
- 2.1 AbortPolicy 抛出异常。
- 3.自定义策略
JDK内置的策略均实现了RejectExecutionHandler接口。
自定义策略实现RejectExecutionHandler接口即可。
public interface RejectedExecutionHandler {
/**
* Method that may be invoked by a {@link ThreadPoolExecutor} when
* {@link ThreadPoolExecutor#execute execute} cannot accept a
* task. This may occur when no more threads or queue slots are
* available because their bounds would be exceeded, or upon
* shutdown of the Executor.
*
* <p>In the absence of other alternatives, the method may throw
* an unchecked {@link RejectedExecutionException}, which will be
* propagated to the caller of {@code execute}.
*
* @param r the runnable task requested to be executed
* @param executor the executor attempting to execute this task
* @throws RejectedExecutionException if there is no remedy
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
处理方法会接受线程任务,和线程池,用户可自行处理。
8.在工作中单一的/固定数的/可变的三种创建线程池的方法,怎么用?
答案是一个都不用,生产上都用自定义的线程池。
- 1.Executors中JDK提供的为什么不用?
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors返回的线程池对象的弊端如下:- 1)FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- 2)CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
- 2.线程资源必须通过线程池提供,不允许在应用中显示定义创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
9.自定义线程池的使用
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(3),
Executors.defaultThreadFactory(),
//默认抛出异常
//new ThreadPoolExecutor.AbortPolicy()
//回退调用者
//new ThreadPoolExecutor.CallerRunsPolicy()
//处理不来的不处理
//new ThreadPoolExecutor.DiscardOldestPolicy()
new ThreadPoolExecutor.DiscardPolicy()
);
//模拟10个用户来办理业务 没有用户就是来自外部的请求线程.
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
//threadPoolInit();
}
10.如何合理配置线程池
- 1.CPU密集型
System.out.println(Runtime.getRuntime().availableProcessors());查看CPU核数
CPU密集型的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论开几个模拟的线程该任务都不可能得到加速。因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:(因为任务偏计算,阻塞相对少,过多的线程或造成CPU频繁的上下文切换,反而影响效率)
一般公时:CPU核心数+1个线程的线程池 - 2.IO密集型
- 2.1由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2
- 2.2IO密集型,即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要配置线程数:
参考公式:CPU核数/1-阻塞系数 阻塞系数在0.8-0.9之间
比如8核CPU:8/1-0.9=80个线程数