本文主要介绍线程池的基本使用
上述其他介绍在上一篇文章中:实现线程的多种方式&锁的介绍&ThreadLocal&线程池 详细总结(上)-CSDN博客
线程池
5.1、为什么使用线程池
线程池可以看做是管理了
N
个线程的池子,和连接池类似
5.2、认识线程池
5.2.1、线程池继承体系
在
Java 1.5
之后就提供了线程池
ThreadPoolExecutor
,它的继承体系如下:
ThreadPoolExecutor :线程池
Executor: 线程池顶层接口,提供了
execute
执行线程任务的方法
Execuors: 线程池的工具类,通常使用它来创建线程池
示例:
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0 ; i < 200 ; i++){
executorService.execute(new Runnable() {
@Override
public void run() {
//有5个线程在执行
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":线程执 行了...");
}
});
}
}
}
运行结果:
5.3、线程池原理
5.3.1、执行流程
我们以一个生活中的举例来理解:
1.
老陈要开软件公司,合伙几个核心的程序员做开发 :
(
线程核心数
)
2.
新的项目过来一个人接收一个项目去做,没有人手了,把新进来的项目放入项目排队池
(
任务队列
)
3.
如果项目队列中的任务过多,需要招聘一些临时的程序员
(
非核心线程
)
,但是规定所有的开发总人数不能50(
最大线程数
)
4.
如果新的项目进来,核心程序员和临时程序员都没有人手了,并且项目队列也放满了,新来的项目该如何处理呢?
1
、拒绝
2
、丢弃老的项目做新的项目
3
、老陈自己做新的项目
线程提交优先级:核心
->
队列
->
非核心
线程执行优先级:核心
->
非核心
->
队列
1
、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2
、当调用
execute()
方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于
corePoolSize
,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于
corePoolSize
,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于
maximumPoolSize
,那么还是要创建非核
心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量等于
maximumPoolSize
,那么线程池会抛出异常
RejectExecutionException
。
3
、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4
、当一个线程无事可做,超过一定的时间(
keepAliveTime
)时,线程池会判断,如果当前运行的线程数大于
corePoolSize
,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到
corePoolSize
的大小。
5.3.2、线程池核心构造器
线程池源码
ThreadPoolExecutor
构造器:
线程池7个参数的构造器非常重要:
1
、
CorePoolSize:
核心线程数,不会被销毁
2
、
MaximumPoolSize :
最大线程数
(
核心
+
非核心
)
,非核心线程数用完之后达到空闲时间会被销毁
3
、
KeepAliveTime:
非核心线程的最大空闲时间,到了这个空闲时间没被使用,非核心线程销毁
4
、
Unit:
空闲时间单位
5
、
WorkQueue:
是一个
BlockingQueue阻塞队列,超过核心线程数的任务会进入队列排队
SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执
行新来的任务;
LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为
Integer.MAX_VALUE
;
ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小
6
、
ThreadFactory
:线程工厂,用于创建线程池中线程的工厂方法,通过它可以设置线程的命名规则、优先级和线程类型。使用
ThreadFactory
创建新线程。 推荐使用
Executors.defaultThreadFactory
7
、
Handler:
拒绝策略,任务超过 最大线程数
+
队列排队数 ,多出来的任务该如何处理取决于
Handler
AbortPolicy丢弃任务并抛出
RejectedExecutionException
异常;
DiscardPolicy丢弃任务,但是不抛出异常;
DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务;
CallerRunsPolicy由调用线程处理该任务
可以定义和使用其他种类的
RejectedExecutionHandler
类来定义拒绝策略。
5.4、常见四种线程池
Jdk
官方提供了常见四个静态方法来创建常用的四种线程
.
可以通过
Excutors
创建
1. CachedThreadPool
:可缓存
2. FixedThreadPool
:固定长度
3. SingleThreadPool
:单个
4. ScheduledThreadPool
:可调度
5.4.1、CachedThreadPool
可缓存线程池,可以无限制创建线程
根据源码可以看出:
这种线程池内部没有核心线程,线程的数量是有限制的最大是Integer
最大值
在创建任务时,若有空闲的线程时则复用空闲的线程(
缓存线程
)
,若没有则新建线程
没有工作的线程(闲置状态)在超过了60S
还不做事,就会销毁
适用:执行很多短期异步的小程序或者负载较轻的服务器
实战:
运行结果:
5.4.2、FixedThreadPool
根据源码可以看出:
该线程池的最大线程数等于核心线程数,所以在默认情况下,该线程池的线程不会因为闲置状态超
时而被销毁
如果当前线程数小于核心线程数,并且也有闲置线程的时候提交了任务,这时也不会去复用之前的
闲置线程,会创建新的线程去执行任务(必须达到最大核心数才会复用线程)。如果当前执行任务
数大于了核心线程数,大于的部分就会进入队列等待。等着有闲置的线程来执行这个任务
适用:执行长期的任务,性能好很多
实战:
public class fixedThreadPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 150; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
//有5个线程在执行
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":线程执 行...");
}
});
}
}
}
运行效果:
5.4.3、SingleThreadPool
根据源码可以看出:
有且仅有一个工作线程执行任务
所有任务按照指定顺序执行,即遵循队列的入队出队规则。
适用:一个任务一个任务执行的场景。 如同队列
实战:
运行结果:
5.4.4、ScheduledThreadPool
根据源码可以看出:
1. DEFAULT_KEEPALIVE_MILLIS就是默认
10L
,这里就是
10
秒。这个线程池有点像是
CachedThreadPool和
FixedThreadPool
结合了一下
2. 不仅设置了核心线程数,最大线程数也是Integer.MAX_VALUE
3.
这个线程池是上述4
个中唯一一个有延迟执行和周期执行任务的线程池
4. 适用:周期性执行任务的场景(定期的同步数据)
实战:
public static void main(String[] args) {
//带缓存的线程,线程复用,没有核心线程,线程的最大值是 Integer.MAX_VALUE
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
//延迟 n 时间后,执行一次,延迟任务
executorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println("延迟任务执行.....");
}
}, 10, TimeUnit.SECONDS);
//定时任务,固定 N 时间执行一次 ,按照上一次任务的开始执行时间计算下一次任务开始时间
executorService.scheduleAtFixedRate(() -> {
System.out.println("定时任务 scheduleAtFixedRate 执行time:" + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 1, 1, TimeUnit.SECONDS);
//定时任务,固定 N 时间执行一次 ,按照上一次任务的结束时间计算下一次任务开始时间
executorService.scheduleWithFixedDelay(() -> {
System.out.println("定时任务 scheduleWithFixedDelay 执行time:" + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 1, 1, TimeUnit.SECONDS);
}
运行结果:
总结:除了
new ScheduledThreadPool
的内部实现特殊一点之外,其它线程池内部都是基于
ThreadPoolExecutor
类(
Executor
的子类)实现的。
5.4.5、自定义ThreadPoolExecutor
public static void main(String[] args) {
//核心 4 个 ,最大 10 个 ,30s的空闲销毁非核心6个线程, 队列最大排队 10 个
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 10, 30L, TimeUnit.SECONDS, //超过核心线程数量的线程 30秒之后会退出
new ArrayBlockingQueue<Runnable>(10), //队列排队10个
new ThreadPoolExecutor.DiscardPolicy()); //任务满了就丢弃
for (int i = 0 ; i < 210 ; i++){
int finalI = i;
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
//始终只有一个线程在执行
System.out.println(Thread.currentThread().getName()+":线程执 行..."+ finalI);
}
});
}
}
分析:
上面示例中,是创建了
210
个线程,但是从结果来看,却只有
10
个线程,就是因为有下面的设置:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 10,
30L, TimeUnit.SECONDS, //超过核心线程数量的线程 30秒之后会退出
new ArrayBlockingQueue<Runnable>(10), //队列排队10个
new ThreadPoolExecutor.DiscardPolicy()); //任务满了就丢弃
这里设置了最大线程是
10
个,如果多了就会排队
10
个,再多的线程就会直接丢弃
5.5、在ThreadPoolExecutor类中几个重要的方法
Execute
:方法实际上是
Executor
中声明的方法,在
ThreadPoolExecutor
进行了具体的实现,这个方法是
ThreadPoolExecutor
的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
Submit
:方法是在
ExecutorService
中声明的方法,在
AbstractExecutorService
就已经有了具体的实现,在
ThreadPoolExecutor
中并没有对其进行重写,这个方法也是用来向线程池提交任务的,实际上它
还是调用的
execute()
方法,只不过它利用了
Future
来获取任务执行结果。
Shutdown
:不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
shutdownNow
:立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
isTerminated
:调用
ExecutorService.shutdown
方法的时候,线程池不再接收任何新任务,但此时线程池并不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。在调用
shutdown
方法
后我们可以在一个死循环里面用
isTerminated
方法判断是否线程池中的所有线程已经执行完毕,如果子
线程都结束了,我们就可以做关闭流等后续操作了。
5.6、如何设置最大线程数
5.6.1、CPU密集型
定义:
CPU
密集型也是指计算密集型,大部分时间用来做计算逻辑判断等
CPU
动作的程序称为
CPU
密集型任务。该类型的任务需要进行大量的计算,主要消耗
CPU
资源。 这种计算密集型任务虽然也可以用多任务
完成,但是任务越多,花在任务切换的时间就越多,
CPU
执行任务的效率就越低,所以,要最高效地利
用
CPU
,计算密集型任务同时进行的数量应当等于
CPU
的核心数。
特点:
1. CPU
使用率较高(也就是经常计算一些复杂的运算,逻辑处理等情况)非常多的情况下使用
2.
针对单台机器,最大线程数一般只需要设置为
CPU
核心数的线程个数就可以了
3.
这一类型多出现在开发中的一些业务复杂计算和逻辑处理过程中。
示例:
public class Demo02 {
public static void main(String[] args) {
//自定义线程池! 工作中只会使用 ThreadPoolExecutor
/**
* 最大线程该如何定义(线程池的最大的大小如何设置!)
* 1、CPU 密集型,几核,就是几,可以保持CPU的效率最高!
*/
//获取电脑CPU核数
System.out.println(Runtime.getRuntime().availableProcessors()); //8核
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, //核心线程池大小
Runtime.getRuntime().availableProcessors(), //最大核心线程池大小(CPU密集型,根据CPU核数设置)
3, //超时了没有人调用就会释放
TimeUnit.SECONDS, //超时单位
new LinkedBlockingDeque<>(3), //阻塞队列
Executors.defaultThreadFactory(), //线程工厂,创建线程的,一般不用动
new ThreadPoolExecutor.AbortPolicy()); //银行满了,还有人进来,不处理这个人的,抛出异常
try {
//最大承载数,Deque + Max (队列线程数+最大线程数)
//超出 抛出 RejectedExecutionException 异常
for (int i = 1; i <= 9; i++) {
//使用了线程池之后,使用线程池来创建线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭线程池
threadPool.shutdown(); //(为确保关闭,将关闭方法放入到finally中)
}
}
}
5.6.2、IO密集型
定义:
1
、
IO
密集型任务指任务需要执行大量的
IO
操作,涉及到网络、磁盘
IO
操作,对
CPU
消耗较少,其消耗的主要资源为
IO
2
、我们所接触到的
IO
,大致可以分成两种:磁盘
IO
和网络
IO
:
磁盘 IO
,大多都是一些针对磁盘的读写操作,最常见的就是文件的读写,假如你的数据库、
Redis
也是在本地的话,那么这个也属于磁盘
IO
。
网络 IO
,这个应该是大家更加熟悉的,我们会遇到各种网络请求,比如
http
请求、远程数据库读
写、远程
Redis
读写等等。
特点:
IO 操作的特点就是需要等待,我们请求一些数据,由对方将数据写入缓冲区,在这段时间中,需
要读取数据的线程根本无事可做,因此可以把
CPU
时间片让出去,直到缓冲区写满
既然这样,IO
密集型任务其实就有很大的优化空间了(毕竟存在等待)
CPU 使用率较低,程序中会存在大量的
I/O
操作占用时间,导致线程空余时间很多,所以通常就需要开
CPU
核心数两倍的线程。当线程进行
I/O
操作
CPU
空闲时,线程等待时间所占比例越高,就
需要越多线程,启用其他线程继续使用
CPU
,以此提高
CPU
的使用率;线程
CPU
时间所占比例越
高,需要越少的线程,这一类型在开发中主要出现在一些计算业务频繁的逻辑中
示例:
public class Demo02 {
public static void main(String[] args) {
//自定义线程池! 工作中只会使用 ThreadPoolExecutor
/**
* 最大线程该如何定义(线程池的最大的大小如何设置!)
* 2、IO 密集型 >判断你程序中十分耗IO的线程
* 程序 15个大型任务 io十分占用资源! (最大线程数设置为30)
* 设置最大线程数为十分耗io资源线程个数的2倍
*/
//获取电脑CPU核数
System.out.println(Runtime.getRuntime().availableProcessors()); //8核
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, //核心线程池大小
16, //若一个IO密集型程序有15个大型任务且其io十分占用资源!(最大线程数设置为 2*CPU 数目)
3, //超时了没有人调用就会释放
TimeUnit.SECONDS, //超时单位
new LinkedBlockingDeque<>(3), //阻塞队列
Executors.defaultThreadFactory(), //线程工厂,创建线程的,一般不用动
new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试和最早的竞争,也不会抛出异常
try {
//最大承载数,Deque + Max (队列线程数+最大线程数)
//超出 抛出 RejectedExecutionException 异常
for (int i = 1; i <= 9; i++) {
//使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭线程池
threadPool.shutdown(); //(为确保关闭,将关闭方法放入到finally中)
}
}
}
5.6.3、分析
1
:高并发、任务执行时间短的业务,线程池线程数可以设置为
CPU
核数
+1
,减少线程上下文的切换
2
:
并发不高、任务执行时间长的业务这就需要区分开看了:
a
)假如是业务时间长集中在
IO
操作上,也就是
IO
密集型的任务,因为
IO
操作并不占用
CPU
,所以不要让所有的
CPU
闲下来,可以适当加大线程池中的线程数目,让
CPU
处理更多的业务
b
)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,线程池中的线程数设置得少一些,减少线程上下文的切换(其实从一二可以看出无论并发高不高,对于业务中是否是
cpu
密集还是
I/O
密集的判断都是需要的当前前提是你需要优化性能的前提下)
3
:
并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些
业务里面某些数据是否能做缓存是第一步,我们的项目使用的时
redis
作为缓存(这类非关系型数据库还
是挺好的)。增加服务器是第二步(一般政府项目的首先,因为不用对项目技术做大改动,求一个稳,
但前提是资金充足),至于线程池的设置,设置参考
2
。最后,业务执行时间长的问题,也可能需要分
析一下,看看能不能使用中间件(任务时间过长的可以考虑拆分逻辑放入队列等操作)对任务进行拆分
和解耦。
5.6.4、总结
1.
一个计算为主的程序(
CPU
密集型程序),多线程跑的时候,可以充分利用起所有的
CPU
核心
数,比如说
8
个核心的
CPU ,
开
8
个线程的时候,可以同时跑
8
个线程的运算任务,此时是最大效
率。但是如果线程远远超出
CPU
核心数量,反而会使得任务效率下降,因为频繁的切换线程也是
要消耗时间的。因此对于
CPU
密集型的任务来说,线程数等于
CPU
数是最好的了。
2.
果是一个磁盘或网络为主的程序(
IO
密集型程序),一个线程处在
IO
等待的时候,另一个线程还可以在
CPU
里面跑,有时候
CPU
闲着没事干,所有的线程都在等着
IO
,这时候他们就是同时的
了,而单线程的话此时还是在一个一个等待的。我们都知道
IO
的速度比起
CPU
来是很慢的。此时线程数等于
CPU
核心数的两倍是最佳的。