一、线程池
1.1 什么是线程池
线程池是一种管理和复用线程的机制,线程池会提前创建好一定数量线程存储在池中,当有任务要执行时,就从线程池中取出一个线程来执行任务,任务执行结束后,就把该线程放回线程池中供其他线程使用
1.2 为什么要用线程池
我们来看看线程池的优势
- 降低资源消耗:线程池可以复用已创建的线程,显著减少了创建和销毁线程的系统开销;按照以前的做法,每有一个任务都要创建一个线程,任务结束后销毁线程,线程池则可以循环复用
- 提高响应速度:线程池中的线程都是提前创建好的,随需随取,任务完成后放归线程池,不需要等待线程创建和销毁线程
1.3 ThreadPoolExecutor
ThreadPoolExecutor 是线程池的具体实现——一个类
ThreadPoolExecutor类中的构造方法
所以我们来看最后一个构造方法:
【定义】 corePoolSize:核心线程数,maximumPoolSize:最大线程数
【解释】
- 标准库中将线程分为核心线程和非核心线程,maximumPoolSize = 核心线程数+非核心线程数
- 一个线程池刚被创建的时候,里面的线程数就是核心线程数这么多(假设核心线程数为4,最大线程数为8)
- 线程池会提供一个 submit 方法,其他线程来向线程池中添加任务,如果任务较少,4个线程能够处理的过来,那么线程池就只有这4个线程工作了;如果任务较多,线程池就会自动创建出新的线程来支撑更多的任务,创建的总线程数不能超过最大线程数
【定义】keepAliveTime:允许非核心线程空闲的最大时间,unit:keepAliveTime 的单位
【解释】当非核心线程没有执行任务的时间超过 keepAliveTime,就会被回收,单位表:
【定义】BlockingQueue<Runnable> workQueue:线程池中存放任务的阻塞队列
【解释】后续线程池中的工作线程就会消费这个队列
【定义】ThreadFactory threadFactory:用来创建线程的工厂类
【解释】这里涉及到一种设计模式——工厂模式
工厂模式主要用来解决构造方法创建对象的相关问题,比如:
上述代码中,一个是直角坐标的方式创建一个点对象,一个是极坐标的方式创建一个点对象,但是这两个构造方法无法构成重载,导致编译报错
解决方法如下:
class Point {
//直角坐标
public static Point pointByXY (double x, double y) {
Point p = new Point();
p.setX(x);
p.setY(y);
return p;
}
//极坐标
public static Point pointByRA (double r, double a) {
Point p = new Point();
p.setR(r);
p.setA(a);
return p;
}
}
上述两个方法就是工厂方法,Point就是一个工厂类
其实如果语法层面上不要求构造方法名字必须和类名一样,就不会有上述模式了
threadFactory 创建线程,主要是为了批量的给要创建的线程设置一些属性,将线程的属性提前初始化好(比如名字)
【定义】RejectedExecutionHandler handler:这是一个枚举类型,代表拒绝策略
【解释】当任务队列满了,仍然要继续添加任务时,线程池要采用的拒绝策略
ThreadPoolExecutor.AbortPolicy:被拒绝的任务的处理程序,抛出一个 RejectedExecutionException(让程序员快速直到任务处理不过来了)
ThreadPoolExecutor.CallerRunsPolicy:由添加任务的线程来执行这个任务
ThreadPoolExecutor.DiscardOldestPolicy:丢弃最老的任务,让新任务去队列中排队
ThreadPoolExecutor.DiscardPolicy:丢弃这个任务,按照原来的节奏执行
1.4 标准库中的线程池
Executors 是一个工具类,它提供了一系列静态方法来创建不同类型的线程池,这些线程池都是基于 ThreadPoolExecutor 或其他相关类的封装
以上就是对线程池的构造方法的描述,其实标准库也知道ThreadPoolExecutor使用起来很费劲(构造方法的参数过多)于是标准库自己提供了几个工厂类
可以看一下这些方法的源码:
后面就不展示了,可以自己去看,基本都是基于 ThreadPoolExecutor 的封装
1.5 自己实现线程池
class MyThreadPool {
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
//初始化
public MyThreadPool (int n) {
//
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
try {
while (true) {
//线程刚创建和线程执行完所有任务后都会在这里阻塞
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
}
//提交任务
void submit (Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(10);
for (int i = 0; i < 10000; i++) {
int id = i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello "+ id + ", " + Thread.currentThread().getName());
}
});
}
}
}
代码在执行 main 方法里的创建 MyThreadPool 对象时就已经创建好了10个线程,此时这10个线程都因为阻塞队列为空而处于阻塞状态
向线程池提交10000个任务后,这10个线程就会开始消费阻塞队列中的任务
⬆️共打印10000条
二、定时器
2.1 什么时定时器
定时器类似于一个闹钟,指定一个任务,指定一个时间,该任务不会立即执行,而是等指定的时间过去后再去执行
比如验证码的时效性就可以理解为有定时器
2.2 标准库中的定时器
标准库提供了一个 Timer 类,Timer类中的核心方法是 schedule
TimerTask 是一个抽象类,本质上实现了Runnable接口,把它当成Runnable即可;delay表示多长时间之后执行
public class Demo {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 2000);
}
}
2s后打印"hello"
程序没有结束,因为Timer内部包含一个前台线程,阻塞等待下一个任务,所以阻塞了进程的结束
2.3 模拟实现定时器
实现定时器需要完成2个功能:
- 能够延时执行任务/指定时间执行任务
- 能够管理多个任务
1. 定义一个类表示一个任务:
class MyTimerTask {
//两个属性
private Runnable runnable; //任务
private long time; //这是一个绝对时间,表示什么时候执行任务
//初始化两个属性
public MyTimerTask (Runnable runnable, Long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
//执行任务
public void run() {
runnable.run();
}
//获取任务执行的绝对时间
public long getTime () {
return time;
}
}
2. 定义一个优先级队列来管理任务
class MyTimer {
PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
public void schedule (Runnable runnable, long delay) {
MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
queue.offer(myTimerTask);
}
}
让任务按照各自的执行时间作为优先级的标准,队首元素就是最早要执行的任务,线程只需要关注队首元素即可(因为时间最早要执行的任务还没执行,其他任务肯定时间还没到)
【引入比较规则】
为了以执行时间作为优先级标准,应该在 MyTimeTask 类中引入比较规则
3. 管理好任务后,需要有一个线程来执行这些任务
写在 MyTimer 的构造方法里
public MyTimer () {
Thread t = new Thread(() -> {
while (true) {
if (queue.size() == 0) {
continue;
}
//取出任务
MyTimerTask task = queue.peek();
//判断时间
long curTime = System.currentTimeMillis();
if(curTime >= task.getTime()) {
task.run();
queue.poll();
} else {
//时间还没到
continue;
}
}
});
}
当队列为空时,代码会一直进行循环,什么也不干,当队列不为空时,代码会取出一个任务,判断时间有没有到,如果到了,就执行,没到就再入循环
【线程不安全】
上述代码中存在线程不安全问题
主线程中,schedule方法内存在入队操作;t线程中,存在出队操作;两个线程针对操作同一个队列就会引发线程不安全的问题
比如:t线程检查当前队首元素到时间了可以执行,执行完后要将该队首元素取出,但是再取出之前,主线程又向队列中添加了一个新的任务,新任务的时间又正好早于要取出的任务,那么此时新任务就成了队首元素,这时就将新入队的任务在还没执行的情况下就出队了
基于上述原因要进行加锁:
【忙等】
代码中还存在一处不合理的地方:在t线程中,判断任务的执行时间还没到就要再入循环,在下一轮循环又要把判断队列为空、取出队首元素、计算时间这些操作再做一遍,使得代码在持续消耗CPU,但又没有执行任务,所以要在等待的时间里,然线程阻塞,释放CPU资源
这里使用 wait()
🙉本篇文章到此结束