Java多线程—线程池
1.1 线程池引入的原因
Java编程世界中是不鼓励多进程编程模式的,因为进程频繁创建与销毁带来了巨大的开销,线程作为轻量级的进程可以有效缓解此问题,但是现在随着线程数目的增多,所带来的的开销也不可忽略!解决的策略有如下两种:
- 创建轻量级的线程,又被称为协程/纤程(即Java21引入的虚拟线程)
- 使用线程池管理线程,减少启动销毁线程的开销
因此我们此处引入线程池的目的就是有效减少频繁创建线程、销毁线程的带来的开销。
线程池的实现思路:
- 提前准备好多个线程存放到线程池中
- 若添加任务,则直接使用线程池中的线程完成该任务,无需再创建线程
- 任务执行完毕后,无需将线程销毁,该线程仍交由线程池进行管理
1.2 Java线程池的参数介绍
JVM实现了对线程池的支持,JavaAPI提供了一个线程池实现类ThreadPoolExecutor
,其中我们需要了解该类的构造方法中的参数,这对于我们理解线程池该数据结构具有一定意义。
我们从上述官方文档中可以抽取出核心的参数:
参数名 | 含义 |
---|---|
int corePoolSize | 最大核心线程数 |
int maximumPoolSize | 最大线程数 |
long keepAliveTime | 线程数存活时间 |
TimeUnit unit | keepAliveTime的单位 |
BlockingQueue workQueue | 阻塞队列(存放缓存任务) |
RejectedExecutionHandler | 拒绝执行处理器 |
下面我们将对各个参数的含义做出解释:
-
corePoolSize
:表示该线程池中的核心线程数目的最大值 -
maximumPoolSize
:表示该线程池中线程数目的最大值 -
workQueue
:用户缓存任务的阻塞队列
我们通过向线程池中添加任务来说明三者之间的关系
(1) 如果此时没有线程空闲并且线程数小于corePoolSize
,那么就添加新的线程并由该线程处理该任务
(2) 如果此时没有线程空闲,且当前线程数等于corePoolSize
,但是阻塞队列workQueue
此时未满,那么就将该任务添加到阻塞队列中,等到核心线程空闲时进行处理,不添加新的线程。
(3) 如果此时没有线程空闲,并且阻塞队列已经满了,但是线程数目小于maximumPoolSize
,此时就添加新的线程执行任务
(4) 如果此时没有线程空闲,且阻塞队列已满,且池中线程数等于maximumPoolSize
,此时则根据RejectedExecutionHandler
指定的策略拒绝
我们举个生动形象的栗子:
一个公司正常运转,①情景一:如果此时来了一个任务,但是公司员工都处于繁忙状态中,此时老板发现正式工还没有招满,于是老板就招了一个正式工来完成这个任务。此时这个正式工就是核心线程。②情景二:此时又来了一个新的任务,但是此时正式员工已经招满了并且均处于忙碌状态。于是老板决定先将该任务搁置写在备忘录上,这个备忘录就是workQueue
,等到正式员工有人忙完手头上的工作时,老板就可以将备忘录上的任务指派给他。③情景三:但是到了年底,正式工满员且都非常忙碌,备忘录上的清单都快列不下了,老板一看不行呀,就招了几个临时工来完成任务,这些临时工就是临时线程。
keepAliveTime
:表示空闲线程的存活时间unit
:表示keepAliveTime的单位
我们继续在上述案例的基础上解释这两个参数的实际含义,接上文老板招了几个临时工完成任务,此后没有新增任务,随着员工各自处理完了手头上的工作。一定有员工闲下来了,但是老板为了节省成本想辞退空闲的员工,但是又担心之后任务是否又会激增,于是老板想了一个策略,若员工空闲时间超过了keepAliveTime
就辞退该员工。
-
ThreadFactory
:指定创建线程的工厂(工厂模式) -
RejectedExecutorHandler
:指定拒绝策略
为了解释RejectedExecutionHandler
的含义,我们在上述案例的基础上扩展新的情景:
此时公司的员工数目已达上限(正式员工+临时工),并且此时备忘录也存放不下了,此时又来了新的任务,老板只能含泪拒绝执行该任务。但是拒绝是一门艺术,如何采用拒绝的策略是有讲究的!
策略 | 含义 |
---|---|
ThreadPoolExecutor.AbortPolicy() | 抛出RejectedExecutionException异常 |
ThreadPoolExector.CallerRunsPolicy() | 由提交任务的线程处理该任务 |
ThreadPoolExecutor.DiscardPolicy() | 抛弃当前任务 |
ThreadPoolExecutor.DiscardOldestPolicy() | 抛弃最先提交但仍未执行的任务 |
其中AbortPolicy
抛出异常,由开发人员针对实际情况进行处理,CallerRunsPolicy
表示该任务线程池不进行处理,交由提交线程的任务进行处理,DiscardPolicy
表示线程池按照原来的策略进行指定,新任务丢弃不处理,DiscardOldestPolicy
表示线程池处理新任务,但是从原来任务中取出最先提交但是并未执行的任务选择抛弃它
1.3 Java线程池的使用
在1.2中我们提到Java提供了线程池的实现类ThreadPoolExecutor
,但是我们还是习惯上使用Executors
该类作为线程池,该类实际上是对ThreadPoolExecutor
的再次封装
public class ThreadDemo01 {
public static void main(String[] args) {
// 1. 创建内置4个线程的线程池对象
ExecutorService executorService = Executors.newFixedThreadPool(4);
// 2. 向线程池提交任务
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("我是一个任务....");
}
});
}
}
其中使用步骤如下:
- 创建
Executors
线程池对象Executors.newSingleThreadPool
:创建带有单个线程的线程池Executors.newFixedThreadPool
:创建指定线程数目的线程池Executors.newCachedThreadPool
:创建可以动态扩容的线程池Executors.newScheduledThreadPool
:创建延时执行功能的线程池
- 得到
ExecutorService
对象 - 通过
ExecutorService.submit
可以注册一个任务到线程池中
1.4 模拟实现线程池
public class MyThreadPoolExecutor {
private BlockingQueue<Runnable> blockingQueue;
public MyThreadPoolExecutor(int nThreads) {
this.blockingQueue = new ArrayBlockingQueue<>(1000);
for (int i = 0; i < nThreads; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// TODO:
while (true) {
// 从阻塞队列中获取任务并执行
try {
Runnable top = blockingQueue.take();
top.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
}
}
/**
* 添加任务
* @param runnable
*/
public void submit(Runnable runnable) {
try {
this.blockingQueue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述代码中我们模拟实现了一个线程池对象
实现步骤如下:
- 内置属性
BlockingQueue<Runnable>
阻塞队列用来缓存任务 - 构造方法我们创建了
nThread
数量的线程,并且执行run方法,我们让每个线程不断扫描阻塞队列,取出队头元素并执行其中的方法,由于阻塞队列BlockingQueue
是线程安全并且带有阻塞功能,所以我们无需手动加锁,也无需判断队列是否为空 submit
方法是用来提交任务的,该方法将提交的任务放入阻塞队列中,由于阻塞对象是线程安全并且带有阻塞功能的,所以这里也无需担心队列满的情况,也不用手动加锁。
测试上述代码:
public class MyThreadPoolExecutorTest {
public static void main(String[] args) {
// 1. 创建线程池对象
MyThreadPoolExecutor myThreadPoolExecutor = new MyThreadPoolExecutor(4);
// 2. 添加任务
myThreadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println("我是一个任务...");
}
});
}
}
可以正常运行!这样我们就模拟实现了一个线程池对象