文章目录
一. 线程池的引入
引入线程, 是为了解决在高并发编程是,可以避免频繁地创建销毁进程, 线程更加轻量级
但是随着时代的发展, 频繁创建销毁线程, 开销也变得越来越明显
那么, 如优化呢?
- 线程池
- 协程
为什么引入线程池 / 协程后, 能够提升效率?
最关键的要点:
直接创建销毁线程, 是需要用户态 + 内核态 配合完成工作的
线程池 / 协程 创建和销毁线程, 只通过用户态即可, 不需要内核态的配合
直接调用api, 创建销毁线程, 这个过程需要内核完成, 但是内核完成的工作很多时候是不太可控的
如果使用线程池, 提前把线程创建好, 方法哦用户态代码中写的数据结构中去, 后面需要用到线程时, 随时从池子中取, 用完了放回池子里面去, 这个过程, 完全是用户态代码, 不需要和内核进行交互
二. 线程池的七个参数(面试题)
ThreadPoolExecuter线程池
可以看到有四种构造方法, 最多有七个参数
int corePoolSize, int maximumPoolSize
corePoolSize是核心线程数
maximumPoolSize是最大线程数
标准库中的线程池是这样设定的, 把线程分为两类:
- 核心线程 ---- corePoolSize
- 非核心线程
maximumPoolSize就是 核心线程数 + 非核心线程数
线程池具有动态扩展功能
一个线程刚被创建出来的时候, 里面就包含核心线程数这么多的线程(假设是4)
线程池里面提供一个submit方法, 往里面添加任务, 每个任务就是一个Runnable对象
如果当前添加的任务比较少, 4个线程就足以能够处理, 就只有4个线程在工作
如果添加的任务多, 4个线程就处理不过来了
此时, 线程池就会自动创建出新的线程, 来支撑更多的任务
但是,创建出来的线程总数, 不能超过 最大线程数
过段时间, 任务没那么多了, 非核心线程就会被释放掉, 至少保证线程池中线程数目不少于核心线程数
实际开发中, 线程数应该设置成多少合适?
不仅仅和你的电脑配置有关(cpu核心数), 还和程序的实际特点有关
极端一点, 程序可以分成两类:
- cpu密集型程序
这种类型的程序, 线程数就不应该设置超过cpu逻辑核心数, 以防cpu吃不消- IO密集型程序
这种类型的程序, 线程数就可以多弄几个, cpu也处理的过来
但是这两种类型都太理想化了, 实际的的程序, 都很密集
实际上, 没有固定的答案, 根据实验的方式, 找到一个合适的值
对程序性能进行测试, 测试过程中, 设置不同的线程池的数值, 最终根据实际程序的响应速度和系统开销, 综合权衡, 找到一个你觉得最合适的值即可
long keepAliveTime, TimeUnit unit
keepAliveTime 表示的是非核心线程数允许空闲的最大时间
unit 表示时间单位
非核心线程数, 在线程池不忙的时候, 不是立即回收的, 需要设置保留时间, 假设设置3s, 如果3s内, 飞核心线程数没有任务执行, 此刻就可以回收了
BlockingQueue workQueue
workQueue 表示 一个阻塞队列, 存放线程池任务的队列
线程池会提供一个submit方法, 让其他线程把任务提交给线程池
那么线程池的内部, 就需要一个队列这样的数据结构, 把要执行的任务保存起来
后续线程池内部的工作的线程, 就消费这个队列, 从而完成具体任务的实现
ThreadFactory threadFactory
threadFactory 表示 线程工厂
线程工厂, 主要是为了批量的给要创建的线程设置一些属性啥的
在他的工厂方法里, 把线程的属性提前初始化好了
工厂模式
工厂模式, 和单例模式一样, 也是一种设计模式
主要解决的问题, 基于构造方法创建对象太坑了的问题
例如: 我们要用一个类表示一个点
此时我们可以通过x y的方式构造
也可以通过极坐标r α的方式构造
此时, 就会出现问题, 两个构造方法的参数类型和参数个数是一样的, 构不成重载, 这是不被允许的
此时就可以通过下面的方式对Point进行初始化:
像makePointByXY, makePointByRA这样的静态方法, 就叫做**“工厂方法”**
把工厂方法用单独的类来实现, 此时这个类就叫**“工厂类”**
这种写代码的套路, 就叫**“工厂模式”**
RejectedExecutionHandler handler
handler 表示 拒绝策略, 是一个枚举类型
如果当前任务队列满了, 仍要继续执行任务, 直接阻塞不太合适, 共提供了四种解决办法
- 直接抛出异常
- 由调用者去执行
submit内部要做的事情不仅仅是入队列, 如果发现队列满了, 并且使用的是拒绝策略, 就会在submit内部自己去执行Runnable的run - 丢弃最早的任务
把任务队列中队首元素舍弃掉 - 丢弃最新的任务
正在submit的这个任务就不要了
三. 线程池的使用
由于标准库的ThreadPoolExecutor用起来比较费劲
于是标准库自己提供了几个工厂类, 对于上述线程池又进一步封装了
创建一个最普通的线程池
能够根据任务的数目, 自动进行扩容线程
返回 ExecutorService 类型
创建固定线程数目的线程池, 此时传的是最大线程数
创建一个只包含单个线程的线程池
创建一个固定的线程个数, 但是任务延时执行的线程池
使用:
调用线程池的submit方法添加任务, 此处给线程池添加了1000个任务
注意, 匿名内部类中不能直接使用i, 变量捕获失败, 因为是个变的值, 可以用id接收一下i
此时, 看到线程池自动创建了好几百个线程
运行起来后, 线程并不会结束, 因为线程池中的线程是前台线程, 会阻止进程结束
四. 自己实现线程池
class MyThreadPool{
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
private int maxPoolSize = 0;
private List<Thread> threadList = new ArrayList<>();
//规定核心线程数和最大线程数
public MyThreadPool(int corePoolSize, int maxPoolSize){
this.maxPoolSize = maxPoolSize;
//一次性创建corePoolSize个线程并启动
for (int i = 0; i < corePoolSize; i++) {
Thread thread = new Thread(() -> {
try {
while(true){
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread.start();
//启动后, 线程不会执行任务, 处于阻塞状态, 因为此时任务队列中还没有任务
threadList.add(thread);
}
}
public void submit(Runnable runnable){
try {
queue.put(runnable);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//这里是我们随便规定的, 如果超过八个线程, 就再创建线程
if(threadList.size() >= 8 && threadList.size() <= maxPoolSize){
Thread thread = new Thread(() -> {
try {
while(true){
Runnable task = queue.take();
task.run();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread.start();
threadList.add(thread);
}
}
}
public class Demo28 {
public static void main(String[] args) {
MyThreadPool myThreadPool = new MyThreadPool(10, 20);
for(int i = 0; i < 1000; i++){
int id = i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello " + id + " " + Thread.currentThread().getName());
}
});
}
}
}