浅理解java中的线程池
Posted 穷少年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅理解java中的线程池相关的知识,希望对你有一定的参考价值。
浅理解java中的线程池
线程池(thread pool)
1.线程池的概念
线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
2.线程池的工作机制
-
在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
-
一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
3.使用线程池的原因
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
- 提供更强大的功能,延时定时线程池。
4.线程池相关概念
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PLiYHPrE-1647320860560)(assets/images/image-20210817125446305.png)]
5.线程池的主要参数
/**
* 从jdk1.5之后引入了线程池类,在package java.util.concurrent包下
**/
public class ThreadPoolExecutor extends AbstractExecutorService
/**
* 核心方法,创建线程池的构造函数,后面提到的newCachedThreadPool(),newFixedThreadPool(),newSingleThreadExecutor()方法创建的线程池,都是依赖这个核心方法,只是各自的参数不同
**/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
参数 | 作用 |
---|---|
corePoolSize (核心线程的数量) | 当向线程池提交一个任务时,若线程池已创建的核心线程数小于corePoolSize ,即便此时存在空闲线程,也会通过创建一个新核心线程来执行该任务,直到已创建的核心线程数大等于corePoolSize 时 |
maximumPoolSize (线程池最大大小) | 线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize ,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。临时线程可创建的数量=maximumPoolSize -corePoolSize |
keepAliveTime (临时线程的空闲时间) | 线程池内,除了核心线程外,都可以算是临时线程,核心线程不能销毁,临时线程可以,临时线程的空闲时间超过keepAliveTime ,那么这个线程就会被销毁 |
workQueue (任务队列) | 用于传输和保存等待执行任务的阻塞队列。 |
threadFactory (线程工厂) | 用于创建新线程。threadFactory 创建的线程也是采用new Thread() 方式,threadFactory 创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。 |
handler (线程饱和策略) | 当线程池和队列都满了,再加入线程会执行此策略,所有的策略实现接口 java.util.concurrent.RejectedExecutionHandler |
6.java中提供的线程池
Executors类提供了4种不同的线程池:newCachedThreadPool
, newFixedThreadPool
, newScheduledThreadPool
, newSingleThreadExecutor
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
public class App
// 所有的线程池,都是通过java.util.concurrent.ThreadPoolExecutor类实现,只是构造参数不同
public static void main(String[] args) throws Exception
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(20);
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
工程方法 | corePoolSize | maximumPoolSize | keepAliveTime | workQueue |
---|---|---|---|---|
Executors.newCachedThreadPool(); | 0 | Integer.MAX_VALUE | 60s | SynchronousQueue(同步,有界,队列) |
Executors.newFixedThreadPool(nThread); | nThread | nThread | 0 | LinkedBlockingQueue(链表,无界,队列) |
Executors.newSingleThreadExecutor(); | 1 | 1 | 0 | LinkedBlockingQueue |
Executors.newScheduledThreadPool(nThread); | nThread | Integer.MAX_VALUE | 10L(ms) | DelayedWorkQueue(implement LinkedBlockingQueue) |
6.1 newCachedThreadPool
用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换),此线程池的corePoolSize
为0,maximumPoolSize
为MAX,所以线程池能无限的创建临时线程,所有的线程在空闲后超过规定存活时间都可以被销毁,但是无限创建临时线程,也以为可能会导致CPU超载
6.2 newFixedThreadPool
创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重);注意:虽然线程的数量是固定的,但是任务队列是无界队列,可以存放任意个任务,也就可能导致OOM(Out Of Memory)异常
6.3 newSingleThreadExecutor
创建一个单线程的线程池,适用于需要保证顺序执行各个任务。任务队列是无界队列,可以存放任意个任务,也就可能导致OOM(Out Of Memory)异常
6.4 newScheduledThreadPool
适用于执行延时或者周期性任务。
7.自定义线程池
我们知道所有的线程池都是通过
java.util.concurrent.ThreadPoolExecutor
类实现,只是构造参数不同,在很多时候,我们的任务场景是不一样的,我们可以通过自定义线程
import java.util.concurrent.*;
public class App
// 核心线程的数量
static int corePoolSize = 10;
// 线程池最大线程数
static int maximumPoolSize = Integer.MAX_VALUE;
// 临时线程规定存活时间
static long keepAliveTime = 60;
// 存活时间的单位
static TimeUnit unit = TimeUnit.MICROSECONDS;
// 任务队列
static SynchronousQueue<Runnable> workQueue = new SynchronousQueue<Runnable>();
// 线程工厂
static ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 线程池拒绝策略
static RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.DiscardOldestPolicy();
public static void main(String[] args) throws Exception
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, rejectedExecutionHandler);
7.1 如何配置线程池
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
混合型(CPU+IO)密集任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
8.线程池的拒绝策略
拒绝策略提供顶级接口
RejectedExecutionHandler
,其中方法rejectedExecution
即定制具体的拒绝策略的执行逻辑。
策略 | |
---|---|
CallerRunsPolicy | 如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务。适合任务较轻的场景,如果任务太重,容易导致程序阻塞 |
AbortPolicy(默认的拒绝策略) | 这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。 |
DiscardPolicy | 丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。 |
DiscardOldestPolicy | 丢弃队列最前面的任务,然后重新提交被拒绝的任务。 |
9.线程池的execute方法与submit方法
线程池内的
execute
方与submit
方法都是用来提交任务到线程池内去执行,最大的区别就是,execute
无返回值,submit
有返回值
区别 | execute | submit |
---|---|---|
参数 | 接受Runnable 接口类型的参数 | 即可接受Runnable 类型参数,关键可以接受Callable 类型参数 |
返回值 | void | 返回Future 类型参数,我们可以通过Future.get 方法获取线程执行完成的返回值,也可以捕捉线程内抛出的异常 |
10.线程池的状态
在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;
下面的几个static final变量表示runState可能的几个取值。
当创建线程池后,初始时,线程池处于RUNNING
状态;
如果调用了shutdown
()方法,则线程池处于SHUTDOWN
状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
如果调用了shutdownNow
()方法,则线程池处于STOP
状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
当线程池处于SHUTDOWN
或STOP
状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED
状态。
提交优先级与执行优先级
线程池的执行流程
提交优先级
- 提交任务给线程池,首先会提交给核心线程区
- 如果核心线程区内的线程都处于忙碌状态,就会将任务放置在任务队列内
- 如果任务队列已满,就会将任务交给临时线程区
执行优先级
- 核心线程不满时,优先创建核心线程,优先执行
- 核心线程满时,创建临时线程执行
- 核心线程与线程执行完毕,后再执行任务队列里面的任务
❓ 常见问题
线程池为什么需要使用(阻塞)队列?
因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。
线程池为什么要使用阻塞队列而不使用非阻塞队列?
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。
使得在线程不至于一直占用cpu资源。
📚references
以上是关于浅理解java中的线程池的主要内容,如果未能解决你的问题,请参考以下文章