先了解一下线程池的经典面试题?
以上就是我们学习的目标
为什么使用线程池?优势是什么?
大部分人的回答都是,降低资源消耗,提高响应速度,提高线程的可管理性。那么为什么会提高呢?
从根源上开始将,首先我们谈一下对java线程概念的理解
线程:是调度CPU资源的最小单位,也叫轻量级进程
线程模型的分类:
用户级线程:
用户程序实现,不依赖操作系统核心,应用提供创建、同步、调度、和管理线程的函数来控制用户线程。不需要用户态/内核态切换,速度快。内核对ULT无感知,线程阻塞则进程(包括它的所有线程)阻塞。
内核级线程:系统内核管理线程(KLT) ,内核保存线程的状态和上下文信息,线程阻塞不会引起进程阻塞。在多处理器系统上,多线程在多处理器.上并行运行。线程的创建、调度和管理由内核完成,效率比ULT要慢,比进程操作快。
java中的JVM使用的是内核级线程模型,所以线程的创建和销毁,需要在用户态和内核态进行来回的切换,来回切换的过程中占用大量的CPU资源。
java的多线程并发执行时的过程
进程切换的时候需要保存当前进程的信息,保存在寄存器或者缓存中,然后再暂存在内核空间的TEE任务状态段,当线程重新获取到CPU时间段,继续执行。
线程池的意义
线程是稀缺资源,它的创建与销毁是比较重且耗资源的操作。而Java线程依赖于内核线程,创建线程需要进行操作系统状态切换,为避免资源过渡消耗需要设法重用线程执行多个任务。线程池就是个线程缓存,负责对线程进行统一分配、调优与监控。
线程池优势:
➢重用存在的线程,减少线程创建,消亡的开销,提高性能
➢提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
➢提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。使用线程池可以进行统一的分配,调优和监控。
回答到这里相信面试官已经非常满意了,我们接着往下看
线程池如何使用?
创建线程池的方法有6种:
1 . Executors.newCachedThreadPool();
创建一个可缓存线程池,应用中存在的线程数可以无限大
2 . Executors.newFixedThreadPool(2)
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3 . Executors.newScheduledThreadPool(2)
创建一个定长线程池,支持定时及周期性任务执行。
4 . Executors.newSingleThreadExecutor();
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,
保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
5 . Executors.newSingleThreadScheduledExecutor();
创建一个单例线程池,定期或延时执行任务。
6 Executors.newWorkStealingPool(3);
创建一个带并行级别的线程池,并行级别决定了同一时刻最多有多少个线程在执行,如不穿如并行级别参数,将默认为当前系统的CPU个数。
《阿里巴巴Java开发手册》 中强制线程池不允许使用Executors 创建,而是通过ThreadPoolExexutor的方式创建,这样的方式让写的同学更加明确线程池的规则,规避资源耗尽的风险。
Executor返回线程池对象的弊端
FixedThreadPool 和 SingleThreadExector : 允许队列的长度为Integer。MAX_VALUE ,可能堆积大量的请求,从而导致OOM
CachedThreadPool 和 ScheduledThreadPool : 允许线程的数量为Integer.MAX_VALUE ,可能会创建大量的线程,从而导致OOM
线程池中重要的参数
源码 (7个参数)
一、corePoolSize 线程池核心线程大小
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
二、maximumPoolSize 线程池最大线程数量
一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
三、keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
四、unit 空闲线程存活时间单位
keepAliveTime的计量单位
五、workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
六、threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
七、handler 拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
①CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个未处理的任务,然后尝试把这次拒绝的任务放入队列。
线程池的工作原理
了解线程池的状态
Running 状态: 可以接收新任务,处理已经添加到队列的任务
shutDown 状态 : 不接受新任务,处理已经添加带队列的任务
stop状态 : 不接受新任务,不处理已经添加到队列的任务,中断正在处理的任务。
Tidying状态 : 所有任务已经终止,ctl纪录的任务数量为0
Terminted 状态 : 线程池终止。
1.当创建线程池后,初始时,线程池处于RUNNING状态;
2.如果调用了**shutdown()**方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
3.如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
4.当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后(ctr纪录的任务数量为0),线程池被设置为TERMINATED状态。
源码
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//判断当前活跃线程数是否小于corePoolSize
if (workerCountOf(c) < corePoolSize) {
//如果小于,则调用addWorker创建线程执行任务
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果大于等于corePoolSize,则将任务添加到workQueue队列。
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果放入workQueue队列失败,则创建非核心线程执行任务
else if (!addWorker(command, false))
//(如果这时创建线程失败(当前线程数大于等于maximumPoolSize时))
调用reject拒绝接受任务
reject(command);
线程池的执行过程:
1 .当新任务来时,execute执行,如果池中的线程数低于核心线程,此时就会创建新的线程来处理当前任务。在添加工作线程时,会检查运行状态和工作线程的数量,在addWorker返回false时,不会添加工作线程.。
2 .即使一个任务可以成功的进入队列,我们仍然需要一个双重检查以便判断是否要新增一个线程(因为在上次检查之后,可能存在线程的死亡)或者线程池已经关闭。所以我们双重检查状态并且在必要的时候进行回滚。
3 .如果任务不能进入队列,我们就尝试增加一个新的线程。如果失败了,我们就知道线程池已经关闭或者是线程池已经饱和(线程池队列已满且工作线程数已达最大)
关于线程池对线程的复用
public void execute(Runnable command){
//获取线程池中线程的数量
int c = ctl.get();
//判断线程数量是否小于核心线程
if (workerCountOf(c) < corePoolSize) {
//小于核心线程就添加一个新线程,执行当前任务
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果线程数量大于核心线程数量,放入队列
if (isRunning(c) && workQueue.offer(command)) {
//再次检查线程状态
int recheck = ctl.get();
//如果线程池关闭,从池中移除当前线程
if (! isRunning(recheck) && remove(command))
//拒绝该任务
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//线程数量大于最大线程数
else if (!addWorker(command, false))
//拒绝任务
reject(command);
}
addWorker方法将当前线程任务当做参数传进Worker对象的构造器中。
private boolean addWorker(Runnable firstTask, boolean core) {
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
t.start();
}
} finally {
}
}
Worker对象就是一个线程。构造器中将当前任务传递给了Worker对象中的firstTask对象然后将当前Worker对象传入线程对象,构造成线程然后启动Worker线程。此时,Worker对象中的run方法就会启动。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
//取出Worker对象中的任务
Runnable task = w.firstTask;
//清空firstTask任务
w.firstTask = null;
try {
//如果task不为空,或者从队列中获取到任务
while (task != null || (task = getTask()) != null) {
//运行当前线程任务
task.run();
}
} finally {
//杀死线程
processWorkerExit(w, completedAbruptly);
}
}
首先判断任务如果不为空,就执行任务的run方法。线程的主要逻辑就是run方法,run方法执行完,该条线程的使命就完成了。至此,我们传进去的当前任务就执行完成了。
但是,线程池的线程复用是咋做的呢?
答案就在于while循环中的getTask方法。
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
//判断allowCoreThreadTimeout的值
//判断线程的数量是否大于核心线程的数量
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
try {
//如果timed为true,就调用poll方法,否则就调用take方法
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
//从队列中取出任务就返回该任务
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
当线程数量大于核心线程的数量或者我们设置了allowCoreThreadTimeOut变量为true,此时就会调用poll方法,在keepAliveTime时间内获取线程,即:有超时时间。如果timed为false,就会使用take方法获取线程,获取不到,就在该位置阻塞,直到有任务进入队列。
至此,我们就明白了。线程池的作者设计了一个循环,不停的从队列中获取任务,只要能从队列中获取到任务,就会一直获取。然后执行线程的run方法。一直使用当前的worker线程执行任务,从而达到了线程的复用。
如何合理设置线程池的参数
首先需要了解CPU密集型和IO密集型的区别
(1)、CPU密集型
CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading 很高。
在多重程序系统中,大部分时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部分时间用在三角函数和开根号的计算,便是属于CPU bound的程序。
CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。
(2)、IO密集型
IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。
I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。
自己测一下自己机器的核数
System.out.println(Runtime.getRuntime().availableProcessors());
1
即CPU核数 = Runtime.getRuntime().availableProcessors()
(4)、分析下线程池处理的程序是CPU密集型还是IO密集型
CPU密集型:corePoolSize = CPU核数 + 1
IO密集型:corePoolSize = CPU核数 * 2
2、maximumPoolSize:最大线程数
当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务。
当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常。
3、keepAliveTime:线程空闲时间
当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize。如果allowCoreThreadTimeout=true,则会直到线程数量=0。
4、queueCapacity:任务队列容量(阻塞队列)
当核心线程数达到最大时,新任务会放在队列中排队等待执行
5、allowCoreThreadTimeout:允许核心线程超时
6、rejectedExecutionHandler:任务拒绝处理器
两种情况会拒绝处理任务:
当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务。
当线程池被调用shutdown()后,会等待线程池里的任务执行完毕再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。
线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常。
ThreadPoolExecutor 采用了策略的设计模式来处理拒绝任务的几种场景。
这几种策略模式都实现了RejectedExecutionHandler 接口。
AbortPolicy 丢弃任务,抛运行时异常。
CallerRunsPolicy 执行任务。
DiscardPolicy 忽视,什么都不会发生。
DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务。
如何来设置呢?
需要根据几个值来决定
tasks :每秒的任务数,假设为500~1000
taskcost:每个任务花费时间,假设为0.1s
responsetime:系统允许容忍的最大响应时间,假设为1s
做几个计算
corePoolSize = 每秒需要多少个线程处理?
threadcount = tasks/(1/taskcost) = tasks*taskcout = (500 ~ 1000)*0.1 = 50~100 个线程。
corePoolSize设置应该大于50。
根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可。
queueCapacity = (coreSizePool/taskcost)*responsetime
计算可得 queueCapacity = 80/0.1*1 = 800。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行。
切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
maxPoolSize 最大线程数在生产环境上我们往往设置成corePoolSize一样,这样可以减少在处理过程中创建线程的开销。
rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理。
keepAliveTime和allowCoreThreadTimeout采用默认通常能满足。
以上都是理想值,实际情况下要根据机器性能来决定。如果在未达到最大线程数的情况机器cpu load已经满了,则需要通过升级硬件和优化代码,降低taskcost来处理。