目录
一、什么是线程池
线程的创建,虽然相比于进程更为轻量,但在频繁创建的情况下,开销也是不可忽略的,为了提高效率,可以引入线程池,提前将线程创建好,放入池子中,后续需要用到线程时,直接从线程池中申请,用完以后还回去即可,这样就免去了一直创建销毁线程,提高了效率
为什么从线程池拿线程比创建线程更为高效?
为了理解这个问题,我们需要理解操作系统中的两个基本概念:用户态和内核态
一个操作系统 = 内核 + 配套的应用程序
内核:操作系统最核心的功能模块集合,例如硬件管理、各种驱动、进程管理......且内核需要给上层应用程序提供支持,例如打印一个hello,应用程序就需要调用内核,然后内核在通过操作系统调用显示器完成打印
应用程序在同一时刻会有许多,但内核只有一个,内核同时给多个应用程序提供服务,有时就会导致服务不那么及时
以一个银行取钱的例子做演示
在这个例子中,柜台的服务员就相当于一个内核,来取钱的人就相当于应用程序
内核态
内核态就是服务员的操作,它的时间不可控,因为服务员也有可能在给你取钱的过程中上个厕所什么的
用户态
时间可控,用户可以自行决定时间
系统创建线程,就是一个内核态的操作,系统从线程池中拿线程,就是用户态的操作,这就是为什么线程池比创建线程更高效的原因
二、线程池的使用
标准库中提供了线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);//创建一个有10个线程的线程池
//添加任务到线程池中
pool.submit(new Runnable(){
@Override
public void run(){
System.out.println("hello");
}
});
这里需要注意的是,此处线程池的创建并非直接new的对象,而是通过Executors类里面的静态方法完成的对象构造,这是典型的工厂模式,工厂模式会在后面详细介绍。
Executors.newFixedThreadPool(10)这个类实际上是对ThreadPoolExecutor这个类进行进一步封装实现的,ThreadPoolExecutor是原装的线程池
线程池的构造方法
线程池的构造方法有很多,查阅官方文档可以查到这些
这里主要介绍最后一条构造方法
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
);
- int corePoolSize:核心线程数
- int maximumPoolSize:最大线程数
如果把线程池比作公司,核心线程数就是其中的正式员工,最大线程数就是正式员工+实习生。我们知道,正式员工需要与公司签订劳动合同,不能随便辞退,而实习生不签订,可以随便辞退,万恶的资本家就是这样剥削我们的,当公司里比较忙时,正式员工+实习生一起上,当比较清闲时,就把实习生给辞了
- long keepAliveTime:存活时间
- TimeUnit unit:存活时间单位
当公司内较为清闲时,公司也无法预知在接下来的时间里会不会突然忙起来,所以不会立马把实习生都辞退了,它们会先让实习生待一段时间,如果一段时间过后,公司仍然清闲,说明接下来的一段时间内大概率是不会忙了,这段时间过后再把实习生给裁掉,这里等待的一段时间就是存活时间,存活时间单位是毫秒、秒、分钟这些单位
- BlockingQueue<Runnable> workQueue:阻塞队列
线程池中要管理很多任务,所以我们可以手动给线程池添加一个阻塞队列,来方便控制、获取队列中的信息,submit方法就是把任务放到队列中
- ThreadFactory threadFactory:线程工厂
工厂模式,就是创建线程的辅助类,无需做过多了解,知道即可
- RejectedExecutionHandler handler:拒绝策略
当线程池中池子满了,再往其中添加任务,应该如何拒绝,下图是标准库中的四种拒绝策略
- 如果满了,继续添加任务,直接抛出异常:例如你今天已经安排满了,你的上司还给你安排了任务,此时你绷不住了,哇的一声哭了出来
- 谁添加,谁负责:你的上司给你安排任务之后,你回怼:我才不去,你自己安排的你自己去吧
- 丢弃最老的任务:丢弃最后执行的安排,把上司安排的任务添加进去(阻塞队列的队首元素)
- 丢弃最新任务:丢弃最先执行的安排,就是不管上司给你安排了什么,统统丢弃,还是该干嘛干嘛。
JAVA标准库创建线程池的四种方式
在java标准库中自带了四种创建线程池的方法
-
newFixedThreadPool(int n):创建一个固定大小的线程池,线程数量为 n 个,所有任务都会放入队列中等待执行。
ExecutorService pool = Executors.newFixedThreadPool(10);//创建十个线程的线程池
-
newCachedThreadPool():创建一个可缓存的线程池,线程数量不定,根据需要动态创建线程,线程空闲超过 60 秒将被终止并回收。
ExecutorService pool = Executors.newCachedThreadPool();//创建缓存线程池,线程数量不固定,动态开辟
-
newSingleThreadExecutor():创建一个单一线程池,线程数量为 1。所有任务都会交给该线程依次执行。
ExecutorService pool = Executors.newSingleThreadExecutor();//创建一个只有一个线程的线程池
-
newScheduledThreadPool(int n):创建一个定长线程池,线程数量为 n 个,可以按照指定的延迟时间或者固定间隔周期执行任务,功能强大。
ExecutorService executor = Executors.newScheduledThreadPool(2);//定长线程池,可以按照指定延迟时间执行任务
代码模拟实现线程池
分析
一个简易完整的线程池,具有以下的属性
- 阻塞队列:用于存放任务
- submit方法,用于添加任务
- 创建固定数量的线程池:用于创建线程
线程池中线程的数量并不是越多越好,在实际开发中,需要参考cpu的核心数量,假设cpu核心数是N,一般设置成N、N+1、2N、1.5N。等等,需要实际测试才能得出具体结论
实现
class MyThreadPool {
private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();//阻塞队列,存放任务
/**
* 用于添加任务到阻塞队列的方法
* @param task:任务
* @throws InterruptedException
*/
public void submit(Runnable task) throws InterruptedException {
queue.put(task);
}
/**
* 构造方法,参数为int,表示创建多少个工作线程
* @param n:线程的数量
*/
public MyThreadPool(int n){
for (int i = 0; i < n; i++) {
Thread t = new Thread(()->{
try {
//一直取任务 使用while循环,当队列中没有任务时不会创建新的线程,所以线程不是恒定不变的,n只是一个上限
while (true){
Runnable runnable = queue.take();//取出任务
runnable.run();//运行
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();//创建完就运行
}
}
}