系列文章目录
第六章 总结线程池
前言
前两章我们学习到了线程安全和使用 synchronized 来进行加锁操作。再次把视角拉回到线程,线程诞生的意义,是因为进程的创建/销毁都太“重量”了。但是如果进一步提高创建和销毁的频率时,那么线程的开销也就不能忽视了。
一、为什么要使用线程池
-
那么,该如何进一步来提高效率呢?
两种典型的办法:
1、协程(轻量级线程),相比于线程,把系统调度的过程给省略了。
2、线程池,在使用第一个线程的时候,提前把2、3、4、5。。。线程创建好,后续如果想使用新的线程,不必重新创建了,直接拿过来就能用了。 -
为啥直接从池子中取的效率比新创建线程的效率更高?
从池子中取,这个动作是纯用户态的操作。
创建新的线程,这个动作则是需要用户态 + 内核态相互配合来完成的操作。
(如果一段程序,是在系统内核中执行,此时就称为“内核态”,如果不是则称为“用户态”) -
这样一种纯用户态的操作,是非常可控的,所以就能优化频繁创建销毁线程的场景。
二、线程池的参数介绍
1、Java 标准库中创建线程池的方式
public class Demo12 {
public static void main(String[] args) {
ExecutorService service1 = Executors.newCachedThreadPool();
ExecutorService service2 = Executors.newFixedThreadPool(10);
ExecutorService service3 = Executors.newSingleThreadExecutor();
ExecutorService service4 = Executors.newScheduledThreadPool(100);
}
}
大家看到上述线程池对象的创建不知道第一眼会不会感觉有点奇怪。是的,线程池对象不是直接 new 出来的,而是通过一个专门的方法,返回了一个线程池对象,这其实就是工厂模式。
创建方式 | 描述 |
---|---|
Executors.newCachedThreadPool(); | 创建线程数目动态增长的线程池 |
Executors.newFixedThreadPool(10); | 创建固定线程数的线程池 |
Executors.newSingleThreadExecutor(); | 创建只包含单个线程的线程池 |
Executors.newScheduledThreadPool(100); | 设定延迟时间后执行命令,或者定期执行命令,是进阶版的 Timer |
3、线程池的七大参数
- 上述这四个工厂方法生成的线程池,本质上都是对 ThreadPoolExecutor 类进行的封装,也就是说== Executors 本质上是 ThreadPoolExecutor 类的封装==。
构造方法中的参数有很多,这也是接下来需要了解的重点
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
int corePoolSize,int maximumPoolSize 描述了线程池中线程的数目。前者是核心线程数,后者是最大线程数。说明这个线程池里线程的数目是可以动态变化的,变化范围就是 [corePoolSize , maximumPoolSize]
-
long keepAliveTime 超时时间,有人使用会自动释放; TimeUnit unit 超时单位
-
BlockingQueue workQueue, 阻塞队列,用来存放线程池中的任务。可以根据需要灵活设置这里的队列是啥
-
ThreadFactory threadFactory, 工厂模式的体现,此处使用 ThreadFactory 作为工厂类,由这个类负责创建线程,主要是为了在创建过程中,对线程的属性作出一些设置。
-
RejectedExecutionHandler handler 线程池的拒绝策略(重要):一个线程池能容纳的任务数量是有上限的,当持续往线程池里添加任务的时候,一旦已经达到上限了,继续再添加,会出现什么效果?
-
拒绝策略:
-
AbortPolicy:直接抛出异常
-
CallerRunsPolicy:新添加的任务,由添加任务的线程负责执行
-
DiscardOldestPolicy:丢弃任务队列中最老的任务
-
DiscardPolicy:丢弃当前新加的任务
-
使用线程池,需要设置线程的数目,数目设置多少合适?
一个线程执行的代码主要有两类:
1.CPU 密集型:代码里主要的逻辑是在进行算术运算/逻辑判断
2.IO 密集型:代码里主要进行的是 IO 操作
所以,代码不同,线程池的线程数目设置不同。正确做法:使用实验的方式,对程序进行性能测试。
三、模拟实现一个线程池
class MyThreadPool3{
//描述一个类,直接使用Runnable
//使用一个数据结构来组着若干个任务
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
//描述一个线程,工作线程的功能就是从任务队列中取任务并执行
static class Worker extends Thread{
//当前线程池中有若干个worker线程,这些线程内部都持有上述的任务队列
private BlockingQueue<Runnable> queue = null;
public Worker(BlockingQueue<Runnable> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
try {
//循环的获取任务队列中的任务
//如果队列为空直接阻塞,如果不为空,就获取里面的内容
Runnable runnable = queue.take();
//获取到之后,就执行任务
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//创建一个数据结构来组织若干个线程
private List<Thread> workers = new ArrayList<>();
public MyThreadPool3(int n){
//在构造方法中创建若干个线程放到数组中
for (int i = 0; i < n; i++) {
Worker worker = new Worker(queue);
worker.start();
workers.add(worker);
}
}
//创建一个方法,允许程序员放任务到线程池中
public void submit(Runnable runnable){
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Demo14 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool = new MyThreadPool(10);
for (int i = 0; i < 100; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello threadpool");
}
});
}
}
}
- 线程池这边会创建是个 worker ,这10个 worker 都持有我的任务池,各自不停地从我的任务池当中拿任务去执行。等到线程池当中没有任务了,这10个 worker 就阻塞了。
四、线程池的工作流程
总结
本章我们学习了线程池的基本知识,线程池是优化频繁创建/销毁线程的场景而诞生的,进一步地提高了效率。Java标准库中的线程池有四大工厂方法,七大参数,四大拒绝策略,这些都是线程池最基本也是最重要的内容。