为什么用线程池?
1.降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
2.提高响应速度,任务来了,直接有线程可用执行,而不是先创建线程,再执行。
3.提高线程的可管理性,线程是稀缺资源,使用线程池可以统一分配调优监控。
线程池7大参数?
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
这几个参数的含义分别是:
参数名 | 参数含义 |
---|---|
corePoolSize | 核心线程数 |
maximumPoolSize | 最大线程数 |
keepAliveTime | 空闲线程存活时间 |
unit | 时间单位 |
workQueue | 线程池任务队列 |
threadFactory | 创建线程的工厂 |
handler | 拒绝策略 |
1.corePoolSize
核心线程数,是指线程池中长期存活的线程数。int 类型。可以理解为长期存在的、“常驻的”线程。就好比是地主家的长工一样,无论这一年的或多还是活少,都不会被辞退。
2.maximumPoolSize
最大线程数,是指线程池允许创建的最大线程数量。最大线程数的数量包含核心线程数。int 类型。比如,地主家的活太多了,光要长工可能干不完,需要一些临时工来帮忙,长工的数量 + 临时工的数量 = 最大线程数。
需要注意的是,在创建线程池的时候,最大线程数 maximumPoolSize 不能小于核心线程数 corePoolSize,否则会在程序运行时报异常:
核心线程数为 2,最大线程数为 1,报 java.lang.IllegalArgumentException 异常,但是最大线程数可以等于核心线程数。
3.keepAliveTime
空闲线程存活时间,指当线程池中没有任务是,会销毁一些线程,销毁的线程数 = maximumPoolSize - corePoolSize。long 类型。
4.unit
时间单位,指空闲线程存活时间单位,与 keepAliveTime 配合使用。单位为 TimeUnit,常用取值有以下 7 个:
参数 | 含义 |
---|---|
TimeUnit.DAYS | 天 |
TimeUnit.HOURS | 小时 |
TimeUnit.MINUTES | 分钟 |
TimeUnit.SECONDS | 秒 |
TimeUnit.MILLSECONDS | 毫秒 |
TimeUnit.MICROSECONDS | 微秒 |
TimeUnit.NANOSECONDS | 纳秒 |
5.workQueue
阻塞队列,指线程池存放任务的队列,用来存储线程池的所有待执行的任务。BlockingQueue 类型。取值有以下几种:
参数 | 含义 |
---|---|
ArrayBlockingQueue | 一个由数组结构组成的有界阻塞队列 |
LinkedBlockingQueue | 一个由链表结构组成的有界阻塞队列 |
SynchronousQueue | 一个不存储元素的阻塞队列,即直接提交给线程,不保持它们 |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列 |
DelayQueue | 一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素 |
LinkedTransferQueue | 一个由链表结构组成的无界阻塞队列。与 SynchronousQueue 类似,还含有非阻塞方法 |
LinkedBlockingDeque | 一个由链表结构组成的双向阻塞队列 |
比较常用的是 LinkedBlockingQueue。线程池的排队策略和该参数息息相关。
6.threadFactory
线程工厂。线程池创建线城市调用的工厂方法,通过此方法可以设置线程的优先级、线程的命名规则以及线程的类型(用户线程还是守护线程)等。ThreadFactory 类型。
public static void main(String[] args) {
// 创建线程工厂
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
// 创建线程池中的线程
Thread thread = new Thread(r);
// 设置线程名称
thread.setName("线程-" + r.hashCode());
// 设置线程优先级
thread.setPriority(Thread.MAX_PRIORITY);
// 设置线程类型(守护线程、用户线程), false-用户线程
thread.setDaemon(false);
return thread;
}
};
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2,
0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), threadFactory);
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println(String.format("线程: %s, 线程优先级: %d",
thread.getName(), thread.getPriority()));
}
});
}
7.handler
拒绝策略。当线程池的任务超出线程池队列可以存储的最大值之后,拒绝任务的策略。RejectedExecutionHandler 类型。取值有:
参数 | 含义 |
---|---|
AbortPolicy | 拒绝并抛出异常 |
CallerRunsPolicy | 使用当前调用的线程来执行此任务 |
DiscardOldestPolicy | 抛弃队列头部(最旧)的一个任务,并执行当前任务 |
DiscardPolicy | 忽略并抛弃当前任务 |
线程池的默认拒绝策略是 AbortPolicy–拒绝并抛出异常。
8.案例
@Test
public void test7(){
ExecutorService executorService=
new ThreadPoolExecutor(3,5,1L, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 10; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"===>办理业务");
});
}
executorService.shutdown();
}
9.线程池的底层工作原理
线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:
-
如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建
新的线程来处理被添加的任务。 -
如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊
缓冲队列。 -
如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数
量⼩于maximumPoolSize,建新的线程来处理被添加的任务。 -
如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等
于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。 -
当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被
终⽌。这样,线程池可以动态的调整池中的线程数
10.线程池中阻塞队列的作用?什么是先添加队列而不是先创建最大线程?
1、⼀般的队列只能保证作为⼀个有限⻓度的缓冲区,如果超出了缓冲⻓度,就⽆法保留当前的任务了,
阻塞队列通过阻塞可以保留住当前想要继续⼊队的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进⼊wait状态,释放cpu资
源。
阻塞队列⾃带阻塞和唤醒的功能,不需要额外处理,⽆任务执⾏时,线程池利⽤阻塞队列的take⽅法挂
起,从⽽维持核⼼线程的存活、不⾄于⼀直占⽤cpu资源。
2、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。
就好⽐⼀个企业⾥⾯有10个(core)正式⼯的名额,最多招10个正式⼯,要是任务超过正式⼯⼈数
(task > core)的情况下,⼯⼚领导(线程池)不是⾸先扩招⼯⼈,还是这10⼈,但是任务可以稍微积
压⼀下,即先放到队列去(代价低)。10个正式⼯慢慢⼲,迟早会⼲完的,要是任务还在继续增加,超
过正式⼯的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时⼯)要是正式⼯加上外包还
是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。
11.线程池中线程复⽤原理
线程池将线程和任务进⾏解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的⼀个
线程必须对应⼀个任务的限制。
在线程池中,同⼀个线程可以从阻塞队列中不断获取新任务来执⾏,其核⼼原理在于线程池对 Thread
进⾏了封装,并不是每次执⾏任务都会调⽤ Thread.start() 来创建新线程,⽽是让每个线程去执⾏⼀
个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执⾏,如果有则直接执⾏,也就是调⽤
任务中的 run ⽅法,将 run ⽅法当成⼀个普通的⽅法执⾏,通过这种⽅式只使⽤固定的线程就将所有任
务的 run ⽅法串联起来。