Java多线程3--设计模式,线程池,定时器
- 一级目录
- Java多线程3
-
- [1. 设计模式--单例模式](#1. 设计模式--单例模式)
-
- [1.1 啥是设计模式?](#1.1 啥是设计模式?)
- [1.2 单例模式的分类](#1.2 单例模式的分类)
-
- [1.2.1 饿汉模式](#1.2.1 饿汉模式)
- [1.2.2 懒汉模式-单线程版](#1.2.2 懒汉模式-单线程版)
- [1.3 单例模式线程安全性的问题](#1.3 单例模式线程安全性的问题)
- [1.4 加锁引入的新问题](#1.4 加锁引入的新问题)
- [2. 阻塞队列](#2. 阻塞队列)
-
- [2.1 ==阻塞队列是什么==](#2.1 ==阻塞队列是什么==)
- [2.2 生产者消费者模型](#2.2 生产者消费者模型)
-
- [2.2.1 优势一](#2.2.1 优势一)
- [2.2.2 优势二](#2.2.2 优势二)
- [2.2.3 生产者消费者模型的缺点](#2.2.3 生产者消费者模型的缺点)
- [2.3 标准库中的阻塞队列](#2.3 标准库中的阻塞队列)
- [2.4 生产者消费者模型的实现](#2.4 生产者消费者模型的实现)
- [2.5 阻塞队列的实现](#2.5 阻塞队列的实现)
- [3. 线程池](#3. 线程池)
-
- [3.1 线程池是什么](#3.1 线程池是什么)
- [3.2 标准库中的线程池](#3.2 标准库中的线程池)
- [3.3 线程池的模拟实现](#3.3 线程池的模拟实现)
- [4. 定时器](#4. 定时器)
-
- [4.1 定时器是什么](#4.1 定时器是什么)
- [4.2 标准库中的定时器](#4.2 标准库中的定时器)
- [4.3 实现定时器](#4.3 实现定时器)
一级目录
二级目录
三级目录
Java多线程3
1. 设计模式--单例模式
1.1 啥是设计模式?
设计模式好比象棋中的棋谱. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.
软件开发中也有很多常见的 "问题场景". 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.
1.2 单例模式的分类
具体的实现方式有很多. 最常见的是 "饿汉" 和 "懒汉" 两种
1.2.1 饿汉模式
类加载的同时, 创建实例
java
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
对于代码中细节的讲解

1.2.2 懒汉模式-单线程版
类加载的时候不创建实例. 第一次使用的时候才创建实例.
java
class Singleton {
比特就业课
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒和饿是相对的
饿是今早创建实例(类加载的过程中)
懒是尽晚的创建实例(甚至可能不创建了,延迟创建)
1.3 单例模式线程安全性的问题
- 上面谈到的单例模式,和线程有什么关系?
- 刚才编写的两份代码(饿汉,懒汉),是否是线程安全的?如果不是,该怎么办?


上面的懒汉模式的实现是线程不安全的
线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例.
一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改instance 了)
加上 synchronized 可以改善这里的线程安全问题
java
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
synchronized(locker){
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
这样的作用

以下的写法和上面的写法相同
java
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这个写法,相当于把locker对象换成了类对象SingletonLazy.class和之前的locker相比,没什么区别
1.4 加锁引入的新问题
java
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
当我们把实例创建好了之后,后续再调用getInstance,此时都是直接执行return,如果只是进行ifpanding+return,纯粹的读操作了。读操作,不涉及到线程安全问题。
但是,每次调用上述的方法,都会触发一次加锁操作,虽然不涉及线程安全问题了,多线程的情况下,这里的加锁,就会互相阻塞-->影响程序的执行效率
因此,我们的解决方案是按需加锁,涉及到线程安全的时候再加锁,不涉及线程安全的时候,就不加锁
java
public static SingletonLazy getInstance(){
if(instance==null){
synchronized (locker){
if(instance==null){
instance = new SingletonLazy();
}
}
}
return instance;
}
以下是对于这段代码的解析:

以往都是单线程的程序,单线程中,连续的两个相同的if,是无意义的,单线程中,执行流就只有一个,上一个if的判定结果和下一个if是一样的
但是在多线程程序中,两次判定之间,可能存在其他线程,就把if中的Instance变量给修改了,也就导致这里的两次if结论可能不同
因此,再仔细分析,上述代码依然存在问题
这里存在指令重排的问题,也是编译器优化的一种体现形式,编译会在逻辑不变的前提下,调整你代码执行的先后顺序,以达到提升程序运行效率的效果。
java
public static SingletonLazy getInstance(){
if(instance==null){
synchronized (locker){
if(instance==null){
instance = new SingletonLazy();
}
}
}
return instance;
}
这里的instance = new SingletonLazy();有三个原子步骤
- 申请内存空间
- 再空间上面构造对象(初始化)
- 内存空间的首地址,赋值给引用变量
正常来说,这三个步骤按照123这样的顺序来执行
但是,在指令重排序下,可能称为123这样的顺序,单线程环境下,123还是132其实没有问题,但是在多线程环境下可能出现bug!!!
我们假设有两个线程,线程 A 和线程 B,同时调用 getInstance(): 线程 A 进入 getInstance(),发现
instance 为 null,进入 synchronized 块。 线程 A 执行 new
SingletonLazy(),但发生了指令重排:
- 它先分配了内存。
- 然后把这个未初始化的内存地址赋值给了 instance。
- 就在它正要执行第三步 "初始化对象" 的时候,线程 A 被 CPU 调度器挂起了(A仍然持有锁locker)
线程 B 此时也调用 getInstance(),执行到第一次检查 if (instance == null)。
它看到 instance 已经不是 null 了(因为线程 A 已经把地址赋值过去了),于是直接跳过 synchronized 块,返回了这个 instance。 线程 B 拿到这个 instance 后,立刻调用 s.func()。但此时,这个对象的构造函数还没执行,所有成员变量都还是默认值(比如 0、null)。
解决方案:在懒汉类中加入
java
private static volatile SingletonLazy instance = null;
因此,总的正确代码为:
java
class SingletonLazy{
private static volatile SingletonLazy instance = null;
private static Object locker = new Object();
//在懒汉模式下,创建实例的时机,是在第一次使用的时候。而不是在程序启动的时候
public static SingletonLazy getInstance(){
if(instance==null){
synchronized (locker){
if(instance==null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
我们总结一下volatile的功能:
- 保证每次读取操作都是都内存,这个问题在Java多线程2--Java的线程安全问题当中有详细的解析
- 关于该变量的读取和修改操作,不会触发重排序
2. 阻塞队列
2.1 阻塞队列是什么
阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则.
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
• 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
• 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型.
2.2 生产者消费者模型
生产者消费者模式就是通过一个容器 来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
生产者消费者模型的两个重要优势
2.2.1 优势一
解耦合(不一定是两个线程之间,也可能是两个服务器之间)


2.2.2 优势二
削峰填谷


一般来说A这种上游服务器,尤其是入口的服务器,干的活更简单,单个请求消耗资源数少,像B这种下游的服务器,通常承担更重的任务量,复杂的计算/存储任务,单个请求消耗资源数量更多。
日常工作中,确实是会给B这样角色的服务器分配更好的机器,即使如此,也很难保证B承担的访问量能够比A更高。
因此,削峰填谷 策略就十分的奏效

2.2.3 生产者消费者模型的缺点
生产者消费者模型肯定也会相应的付出一些代价
- 引入队列之后,整体的结构会更复杂,此时,就需要更对的机器,进行部署,生产环境的结构会更复杂,管理起来更麻烦
- 效率会有影响
2.3 标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
• BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
• put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
• BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
java
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.put("abc");
String elem = queue.take();
System.out.println(elem);
}
运行结果为:
java
public static void main(String[] args) throws InterruptedException {
BlockingDeque<String> queue = new LinkedBlockingDeque<>(100);
//带有阻塞
for (int i = 0; i < 100; i++) {
queue.put("aaa");
}
System.out.println("队列已经满了");
queue.put("aaa");
// String elem = queue.take();
System.out.println("再次尝试put元素");
}


即在第16行被阻塞

注意:
java
new LinkedBlockingDeque<>(100);
如果不设置capacity,默认是一个非常大的数值。。。
实际开发中,一般建议大家能够设置上你要求的最大值,否则你的队列可能变得非常大,导致把内存耗尽,产生内存超出范围这样的异常
2.4 生产者消费者模型的实现
java
public static void main(String[] args) {
BlockingDeque<Integer> queue = new LinkedBlockingDeque<>(1000);
//至少生产者一个线程,消费者一个线程
Thread producer = new Thread(()->{
int n = 0;
while(true){
try {
queue.put(n);
System.out.println("生产元素 "+n);
n++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"producer");
Thread consumer = new Thread(()->{
while(true){
try {
Integer n = queue.take();
System.out.println("消费元素 "+n);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"consumer");
producer.start();
consumer.start();
}
在该程序中,生产者会迅速填满阻塞队列的1000个空间,消费者由于sleep操作,每个约一秒取元素一次,此时生产与消费元素达到了平衡
运行结果为:

2.5 阻塞队列的实现
java
class MyBlockingQueue{
public int head = 0;
public int tail = 0;
public int size = 0;
public String[] data = null;
public MyBlockingQueue(int capacity){
data = new String[capacity];
}
public void put(String elem) throws InterruptedException {
synchronized (this){
while(size>=data.length){
//队列满了,需要阻塞
this.wait();
}
data[tail] = elem;
tail = (tail+1)%data.length;
size++;
this.notify();
}
}
public String take() throws InterruptedException {
synchronized (this){
while(size==0){
this.wait();
}
String elem = data[head];
head++;
if(head>=data.length){
head = 0;
}
size--;
this.notify();
return elem;
}
}
}
public class Demo31 {
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue(1000);
Thread producer = new Thread(()->{
int n = 0;
while(true){
try {
queue.put(n+"");
System.out.println("生产元素 "+n);
n++;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread consumer = new Thread(()->{
while(true){
String n = null;
try {
n = queue.take();
System.out.println("消费元素 "+n);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
producer.start();
consumer.start();
}
}
运行结果:

下图是对于代码中wait/notify操作的解析图

这里还有一个重要的问题,需要注意

API 说明:
在等待时,推荐的做法是在调用 wait 的代码外围,用一个 while 循环来检查等待的条件是否满足,如下例所示。这种方式可以避免由 虚假唤醒 等问题带来的风险。
java
synchronized (obj) {
while (<条件不成立> && <超时未到>) {
long timeoutMillis = ...; // 重新计算超时值
int nanos = ...;
obj.wait(timeoutMillis, nanos);
}
... // 根据条件或超时情况执行相应操作
}
spurious wakeups(虚假唤醒):在没有调用 notify()/notifyAll() 的情况下,线程从 wait() 中被唤醒的现象。这是操作系统层面的正常现象,因此必须用 while 循环重新检查条件,确保唤醒是 "真的" 满足了等待条件。
condition being awaited(等待的条件):线程等待的目标条件(例如 "队列非空""任务到执行时间" 等)。

应为当我们的wait被唤醒后(表明此时size>0),但是仍然存在其他线程执行take操作,使得size0 的情况,因此,需要再确认一次size0, 因此这里使用while循环,而不是if条件语句
3. 线程池
3.1 线程池是什么
我们可以把线程池理解成一个 "线程工厂 + 线程管理中心" :
没有线程池时:每次需要执行任务,都要 新建一个线程→执行任务→销毁线程 ,就像每次打车都要 "造一辆新车→开车→报废车",成本极高。
有了线程池后:线程池会提前 创建一批线程并保存起来核心线程,接到任务时直接分配空闲线程执行,任务完成后线程不销毁,放回池子里等待下一个任务;如果任务太多 ,还会临时创建线程非核心线程,任务少了再销毁这些临时线程。
简单来说:线程池是管理线程的 池子,核心目的是复用线程、控制线程数量、降低资源消耗。
3.2 标准库中的线程池
• 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
• 返回值类型为 ExecutorService
• 通过 ExecutorService.submit 可以注册一个任务到线程池中.
java
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
int id = i;
pool.submit(()->{
System.out.println("hello "+id+","+Thread.currentThread().getName());
});
}
}
我们创建了10个线程,分别分派这10个线程去执行100个提交到线程池中的任务(Runnable类型)
java
pool.submit(()->{
System.out.println("hello "+id+","+Thread.currentThread().getName());
});
就等同于如下代码,也就是提交一个新创建出来的Runnable类型的任务
java
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello "+id+","+Thread.currentThread().getName());
}
});
运行结果为:

。。。。

Executors 创建线程池的几种方式
• newFixedThreadPool: 创建固定线程数的线程池
• newCachedThreadPool: 创建线程数目动态增长的线程池,最大线程数可以是一个很大的数字.
• newSingleThreadExecutor: 创建只包含单个线程的线程池.
• newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer. Executors 本质上是 ThreadPoolExecutor 类的封装
Java标准库中提供了可以直接使用的线程池

经典面试考题:方法中的7个参数都是什么意思
-
int corePoolSize 核心线程数,即使这些线程处于空闲状态(没任务执行),也不会被销毁
-
int maximunPoolSize 线程池允许创建的总线程数上限 即 核心线程+非核心线程数,只有当核心线程全忙 + 任务队列满时,线程池才会创建非核心线程。
-
long keepAliveTime 非核心线程处于空闲状态(没有执行任务)的最大时长,超过这个时间,非核心线程会被销毁
-
TimeUnit unit: 枚举
-
BlockingQueue workQueue: 工作队列,线程池本质上也是生产者消费者模型。调用submit就是在生产任务,线程池里的线程就是在消费任务
-
ThreadFactory threadFactory: 工厂模式,给线程类提供的工厂类
-
RejectedExecutionHandler handler: 拒绝策略。submit把任务添加到任务队列中,任务队列是阻塞队列,当队列满了,此时再添加任务,对于线程池来说 ,不会触发真正的任务入队列操作,不会真阻塞,而是执行拒绝策略相关的代码。

1.ThreadPoolExecutor.AbortPolicy(默认策略)
行为 :直接抛出 RejectedExecutionException 异常,阻止任务提交。
特点 : 最严格的策略,一旦触发就会中断提交流程;如果不捕获异常,会导致提交任务的线程直接崩溃,线程池也可能无法继续工作。
适用场景:对任务执行可靠性要求极高,不允许任务丢失,且能接受抛出异常的场景。2.ThreadPoolExecutor.CallerRunsPolicy 行为:让提交任务的线程(比如主线程)自己去执行这个任务,而不是交给线程池。 特点:
不会丢弃任务,也不会抛异常;会降低提交任务的线程的性能(因为它要去执行任务),相当于 "自我限流";如果线程池已经关闭,任务会被丢弃。
适用场景:对性能要求不敏感,但不允许任务丢失的场景,比如日志记录、简单统计等。
3. ThreadPoolExecutor.DiscardOldestPolicy 行为:丢弃任务队列中最老的、还没被执行的任务 ,然后重新尝试提交当前这个新任务。 特点: 牺牲 "旧任务" 来给 "新任务" 腾位置;
如果线程池已经关闭,新任务会被直接丢弃。 适用场景:任务时效性强,旧任务已经没有执行价值,优先保证新任务执行的场景(比如实时数据更新)。
4. ThreadPoolExecutor.DiscardPolicy 行为:直接静默丢弃当前提交的这个新任务 ,不抛异常,也不做任何处理。 特点: 最 "佛系"
的策略,完全不处理,提交者甚至不知道任务被丢了; 风险极高,容易导致业务数据丢失且难以排查。
适用场景:任务执行与否对业务影响极小,比如心跳检测、统计采样等可丢失的任务。
总结一下:

注:
工厂模式(简单工厂)的核心:把对象创建逻辑抽离到工厂类,调用者通过工厂方法获取对象,无需关注创建细节;
工厂模式解决的问题
我们使用构造方法的名字是固定的,要想提供不同的版本,就要通过重载,而有时候不一定能构成重载,而使用工厂模式就可以就可以解决提供参数类型相同(无法重载的)的不同版本的方法的问题
java
// 点类(产品类):封装点的属性,隐藏构造细节(可选,增强封装性)
class Point {
// 私有化属性,通过getter访问,符合封装原则
private final double x;
private final double y;
// 私有化构造方法:强制外部通过工厂类创建对象(工厂模式的进阶优化)
public Point(double x, double y) {
this.x = x;
this.y = y;
}
// 提供getter方法访问属性
public double getX() {
return x;
}
public double getY() {
return y;
}
// 重写toString,方便打印输出
@Override
public String toString() {
return String.format("(x=%.2f, y=%.2f)", x, y);
}
}
// 工厂类:封装所有Point对象的创建逻辑
class PointFactory {
/**
* 通过直角坐标(x,y)创建点
* @param x 横坐标
* @param y 纵坐标
* @return 直角坐标对应的Point对象
*/
public static Point makePointByXY(double x, double y) {
// 直接创建直角坐标点,逻辑简单
return new Point(x, y);
}
/**
* 通过极坐标(r,a)创建点
* @param r 极径(到原点的距离)
* @param a 极角(角度,如30°、90°,非弧度)
* @return 极坐标转换后的Point对象
*/
public static Point makePointByRA(double r, double a) {
// 核心修复:极坐标转直角坐标的数学公式
// 步骤1:将角度转换为弧度(Math的三角函数默认使用弧度)
double radians = Math.toRadians(a);
// 步骤2:极坐标转直角坐标公式
double x = r * Math.cos(radians);
double y = r * Math.sin(radians);
// 步骤3:创建并返回Point对象
return new Point(x, y);
}
}
// 测试类
public class Demo33 {
public static void main(String[] args) {
// 1. 通过直角坐标创建点
Point pointXY = PointFactory.makePointByXY(10, 20);
System.out.println("直角坐标创建的点:" + pointXY);
// 2. 通过极坐标创建点(r=10,角度30°)
Point pointRA = PointFactory.makePointByRA(10, 30);
System.out.println("极坐标(10,30°)转换的点:" + pointRA);
// 3. 极坐标测试:r=5,角度90°(预期x=0, y=5)
Point pointRA2 = PointFactory.makePointByRA(5, 90);
System.out.println("极坐标(5,90°)转换的点:" + pointRA2);
}
}
运行结果为:

3.3 线程池的模拟实现
java
class MyThreadPool{
//利用阻塞存储任务,最多可以存储1000个任务
BlockingQueue<Runnable> queue = null;
//创建n个线程
public MyThreadPool(int n){
queue = new LinkedBlockingQueue<>(1000);
for (int i = 0; i < n; i++) {
Thread t = new Thread(()->{
//让每个线程持续获得阻塞队列中的任务,并执行
while(true) {
try {
Runnable task = queue.take();
task.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
public void submit(Runnable task) throws InterruptedException {
queue.put(task);
}
}
public class Demo35 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool = new MyThreadPool(10);
for (int i = 0; i < 100; i++) {
int id = i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("id = "+id+Thread.currentThread().getName());
}
});
}
}
}
该代码执行的流程为:
- 首先创先线程池(MyThreadPool),并初始化,由于任务队列中现在没有任务,
Runnable task = queue.take();代码运行到该语句处阻塞 - 创建100个Runnable类型的任务并提交
- 当阻塞队列中有任务时,就开始创建线程,并利用已经创建的10个线程循环执行任务
运行结果为:

线程池的作用:

4. 定时器
4.1 定时器是什么
定时器也是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的代码.
4.2 标准库中的定时器
• 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
• schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
java
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 2000");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 1000");
}
},1000);
System.out.println("hello main");
}
运行结果为:

4.3 实现定时器
定时器的构成
• 创建一个类,表示一个任务
• 定时器中,必须使用一些集合类把这多个任务给管理起来--优先级队列
• 实现schedule方法:把任务添加到队列中即可
• 额外创建一个线程,负责执行队列中的任务(和线程池不同,线程池是只要队列不为空,就立即取任务并执行,此处需要看队首元素的时间,是否到了。时间到,才能执行,时间不到,不能执行)。
java
class MyTimerTask implements Comparable<MyTimerTask>{
private Runnable task;
private long time;
MyTimerTask(Runnable task, long time){
this.task = task;
this.time = time;
}
@Override
public int compareTo(MyTimerTask o){
return (int)(this.time - o.time);
}
public long getTime(){
return time;
}
public void run(){
task.run();
}
}
class MyTimer{
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
Object locker = new Object();
public void schedule(Runnable task, long delay){
synchronized (locker){
MyTimerTask myTimerTask = new MyTimerTask(task, delay+System.currentTimeMillis());
queue.offer(myTimerTask);
locker.notify();
}
}
public MyTimer(){
Thread t = new Thread(()->{
while(true){
synchronized (locker){
try {
while(queue.isEmpty()){
//当队列不为空就唤醒
locker.wait();
}
MyTimerTask task = queue.peek();
//当前的时间小于任务执行的时间,等待,有新任务来的时候会被唤醒
if(task.getTime()>System.currentTimeMillis()){
locker.wait(task.getTime()-System.currentTimeMillis());
}
else{
task.run();
queue.poll();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t.start();
}
}
public class Demo38 {
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 3000");
}
},3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2000");
}
},2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1000");
}
},1000);
}
}
运行结果为:

该代码的运行逻辑:
- 创建MyTimer类定时器,执行其构造方法,创建线程t后阻塞
- 定时器对象timer向队列中提交任务,优先级队列按照等待时间的大小排序
- 由于使用schedule方法,线程t被唤醒,取得队列最首元素将其执行时间与当前时间比较,如果执行时间小于当前时间,线程t仍然会被阻塞(再进行schedule操作时唤醒),否则,任务会被分派到线程上面执行
注意:
- 主线程(提交任务)和t线程(执行任务)的相互唤醒

- t线程的作用:Timer 类中的存在一个 工作 线程, 一直不停的扫描队首元素, 看看是否能执行这个任务