1. volatile关键字
1.1 volatile保证内存可见性
(1)写⼊volatile修饰的变量时
- 改变线程⼯作内存中volatile变量副本的值
- 将改变后的副本的值从⼯作内存刷新到主内存
(2)在读取volatile修饰的变量时
- 从主内存中读取volatile变量的最新值到线程的⼯作内存中
- 从⼯作内存中读取volatile变量的副本
案例(没有加volatile)
package practice.thread; import java.util.Scanner; public class Demo11 { static class Counter { public int flag = 0; } public static void main(String[] args) { Counter counter = new Counter(); Thread t1 = new Thread(() -> { while (counter.flag == 0) { // 什么也不做 } System.out.println("循环结束"); }); Thread t2 = new Thread(() -> { Scanner sc = new Scanner(System.in); System.out.println("输入一个整数:"); counter.flag = sc.nextInt(); }); t1.start(); t2.start(); } }1.2 volatile不保证原子性
package practice.thread; public class Demo9 { public volatile static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(()->{ for(int i=0;i<50000;i++){ count++; } }); Thread t2=new Thread(()->{ for(int i=0;i<50000;i++){ count++; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }给count加上volatile发现结果是小于100000的
2. wait()和notify()
线程是抢占式执行,执行顺序不可控;wait / notify / notifyAll用来协调多个线程执行顺序,做线程间通信。
重点:wait()、notify()、notifyAll()是 Object 的方法,不是 Thread 方法。
2.1 wait
wait 做三件事:
- 当前线程进入该对象的等待队列,暂停执行;
- 释放当前持有的 synchronized 锁;
- 被唤醒后,重新竞争获取锁,拿到锁之后才继续往下执行;
强制要求:wait 必须写在 synchronized 同步块内,否则直接抛IllegalMonitorStateException
wait 结束等待的 4 种条件:
- 其它线程调用该对象 notify(),唤醒线程
- wait(long timeout)超时,时间到自动唤醒
- 线程被interrupt()中断,抛出InterruptedException
- 虚假唤醒(因此业务代码 wait 必须套在while循环,不能用 if)
Object locker = new Object(); synchronized (locker) { System.out.println("等待中"); locker.wait(); System.out.println("等待结束"); }代码现象:执行wait()线程就卡住等待,需要别的线程 notify 唤醒。
2.2 notify
- 也必须在synchronized同步块内调用。
- 作用:唤醒该对象等待队列里的某一个线程;多个线程等待时,随机选一个,没有先来后到。
- 重要:调用notify()不会立刻释放锁;要等执行完同步代码块,退出synchronized,锁才释放,被唤醒的线程才可以去抢锁。
关键点:等待线程和唤醒线程,必须使用同一个对象锁 locker。
2.3 notify All
| 方法 | 行为 |
|---|---|
| notify() | 唤醒等待队列随机 1 个线程 |
| notifyAll() | 唤醒等待队列全部线程,再去竞争锁 |
注意:优先 notify;只有确定需要全部唤醒才用 notifyAll。
2.4 wait与sleep对比
| wait() | sleep() | |
|---|---|---|
| 所属 | Object 实例方法 | Thread 静态方法 |
| 锁要求 | 必须在 synchronized 内部调用,会释放锁 | 不需要锁,不释放锁 |
| 用途 | 线程之间通信协调 | 单纯让线程休眠固定时长 |
| 唤醒方式 | notify /notifyAll/ 超时 /interrupt | 时间到自动结束 /interrupt 打断抛异常 |
2.5 简单案例
package Thread; import java.util.Scanner; public class Demo17 { public static void main(String[] args) throws InterruptedException { Object object = new Object(); Thread t1 = new Thread(() -> { System.out.println("t1 开始"); synchronized (object) { try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("t1 结束"); }); Thread t2 = new Thread(() -> { System.out.println("t2 开始"); synchronized (object) { try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("t2 结束"); }); Thread t3 = new Thread(() -> { System.out.println("t3 开始"); synchronized (object) { try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("t3 结束"); }); Thread t4 = new Thread(() -> { System.out.println("t4 开始"); System.out.println("输入任意内容唤醒线程"); Scanner scanner = new Scanner(System.in); scanner.nextLine(); synchronized (object) { object.notifyAll(); } System.out.println("t4 结束"); }); t1.start(); t2.start(); t3.start(); t4.start(); t1.join(); t2.join(); t3.join(); t4.join(); System.out.println("所有线程执行完毕"); } }结果
3. 单例模式
这是常见的设计模式,单例模式能保证某个类在程序中只存在唯⼀⼀份实例,⽽不会创建出多个实例
3.1 饿汉模式
类加载的同时创建实例
package Thread; // 单例模式 // 饿汉式 // 线程安全 // 可以直接使用 class Singleton{ private static Singleton instance=new Singleton(); public static Singleton getInstance(){ return instance; } private Singleton(){ } }3.2 懒汉模式
类加载的时候不创建实例,第⼀次使⽤的时候才创建实例
class Singleton { private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }改进(解释看3.4)
package Thread; // 单例模式 // 懒汉式 // 线程不安全 // 可以使用synchronized关键字 // 可以使用Double Check Locking(DCL)模式 class Singleton { private static volatile Singleton instance = null; public static Object locker = new Object(); public static Singleton getInstance() { if (instance == null) { synchronized (locker) { if (instance == null) { instance = new Singleton(); } } } return instance; } private Singleton() { } } public class Demo19 { public static void main(String[] args) { Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1 == s2); // Singleton s3=new Singleton(); } }3.3 应用场景解析
一、场景前提
单例类私有构造方法执行磁盘 IO,读取 100GB 超大文件载入内存缓存。 业务目标:全局只加载 1 次,所有线程共享同一份内存数据。 使用实现:未加锁原始懒汉式单例(无任何同步控制)
二、多线程并发调用产生 4 类核心致命问题
内存资源耗尽,触发 OOM 崩溃
- 多个线程同时判断
instance==null,各自执行new创建独立对象 - 每个实例构造方法都会完整加载一份 100G 数据进堆内存
- 2 个线程占用 200G、3 个线程占用 300G,超出物理内存上限,JVM 内存溢出,服务宕机
- 单例 “全局唯一一份内存” 的目标完全失效
磁盘 IO 重复读写,耗时成倍叠加,IO 性能雪崩
- 磁盘(机械硬盘)速度慢,单次读取 100G 耗时 T
- N 个线程并发全量读取,总耗时≈N×T,业务响应时间大幅变长
- 多线程争抢磁盘通道,频繁寻址、IO 队列拥堵,实际读取速度比串行更差
- 磁盘带宽被占满,拖累数据库、日志写入等其他业务
操作系统底层资源句柄泄露
- 每次加载都会打开磁盘文件,每个实例持有独立文件描述符
- 大量并发创建实例耗尽系统最大文件句柄;后续文件读写、Socket 网络全部报错,系统瘫痪
业务数据不一致风险
- 多线程重复加载间隙,如果磁盘源文件发生修改
- 不同实例缓存不同版本的 100G 数据,业务读取结果混乱,产生隐蔽难排查 bug
三、两种可靠解决方案(按需选择)
方案 1:饿汉式单例【追求绝对安全,推荐】
类加载阶段由 JVM 保证只初始化一次,不需要synchronized、不需要volatile,无锁开销;项目启动时一次性加载 100G 数据。
适用:项目启动后一定会使用这份大文件,能够接受启动阶段的内存占用。
方案 2:DCL 双重检查锁 + volatile 懒汉单例【延迟加载】
- 外层
if判空:实例已创建直接返回,避免频繁锁竞争 Singleton.class类锁 + 内层二次判空:防止并发重复创建对象volatile:禁止对象初始化指令重排序,避免拿到半初始化残缺对象
适用:不确定是否一定会用到 100G 数据,希望使用时才加载,节省启动内存。
四、一句话总结
无同步的懒汉单例,多线程加载超大磁盘资源会重复创建重量级对象,造成 OOM、IO 耗时翻倍、句柄耗尽、数据版本错乱;必须使用线程安全的单例,保证实例只初始化一次。
3.4 DCL 双重检查 vs 直接全包锁
DCL 双重检查锁(标准最优)
public static Singleton getInstance() { // 第一层外层判断 if(instance==null){ synchronized (Singleton.class){ // 第二层内层判断 if(instance==null){ instance=new Singleton(); } } } return instance; }直接把整个判断逻辑全包在锁里面(低效加锁)
public static Singleton getInstance() { synchronized (Singleton.class){ if(instance==null){ instance=new Singleton(); } } return instance; }最大区别:锁的竞争频率天差地别
(1)写法 B 全包锁的问题
不管 instance 有没有创建完成,每一次调用 getInstance() 都必须先抢占类锁:
第一次初始化:所有线程排队抢锁,没问题;
后续 99.9% 的请求:对象早就存在了,依然要执行加锁、解锁操作。
锁操作涉及内核态切换、CAS 竞争、阻塞队列维护,高并发下大量调用会产生严重性能损耗。
(2)写法 A DCL 外层判断的作用
外层 if(instance == null) 做过滤:
对象已经实例化 → 直接 return,完全不进入同步代码块,不加任何锁;
只有第一次还未创建的时候,才会去争抢锁。
绝大多数业务场景下,getInstance() 都是重复获取已存在的实例,DCL 几乎无锁开销。
内层第二个 if 的作用(容易忽略)
假设没有内层判断:
if(instance==null){ synchronized (Singleton.class){ instance=new Singleton(); } }场景:
线程 A、线程 B 同时通过外层 if 判断都为 null,A 先拿到锁创建完毕释放锁;
B 接着拿到锁,又会执行一次 new,再次创建第二个对象,单例失效。
内层二次判空就是为了拦截这种情况,锁内再校验一次是否已经被别的线程初始化。
4. 阻塞队列
阻塞队列是⼀种特殊的队列也遵守"先进先出"的原则
- 当队列满的时候,继续⼊队列就会阻塞,直到有其他线程从队列中取⾛元素。
- 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插⼊元素。
阻塞队列典型的应用场景是生产者消费者模型
4.1 生产者消费者模型
⽣产者消费者模式就是通过⼀个容器来解决⽣产者和消费者的强耦合问题。
⽣产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤ 等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取。
优点:
- 减少资源竞争:生产者、消费者职责分离,互不抢占执行逻辑,通过队列解耦同步冲突,大幅降低多线程竞争锁的概率。
补充总结
(1)阻塞队列put、take依旧争抢同一把锁,入队、出队是串行操作,并没有消除锁竞争。
(2)旧模式:生产者消费者混杂争抢业务锁,锁内部包含耗时业务,锁持有时间长,锁冲突激烈。
(3)新模式:竞争收拢到阻塞队列;生产者竞争入队,消费者竞争出队;锁内只执行队列简短存取操作,执行完立即释放锁;业务逻辑放到锁外部,消费者拿到任务后可以并发执行业务。
(4)本质:没有消灭锁,只是缩短锁持有时间,将业务层锁竞争转移到队列层,降低冲突开销。
背诵一句话:
阻塞队列没有消除锁,只是把竞争收拢到队列,锁内只做简短存取,业务放到锁外执行,缩短锁占用时间,减少锁竞争。
- 降低耦合:生产者只负责生产,消费者只负责消费;双方互不依赖互不干涉,修改其中一方不会影响另一方。
- 削峰填谷:瞬时高并发任务先缓冲存放在队列,防止瞬间压垮下游;流量低谷再逐步消费积压任务,实现流量平滑。
缺点:
- 系统复杂度上升:多线程 + 队列阻塞 + 同步等待机制,对比单线程逻辑,调试难度变大,更容易出现并发 Bug。
- 多层队列带来额外开销:队列缓冲链路层数过多,会带来排队延迟、更多上下文切换;如果是分布式队列,还会引入网络 IO 开销。
代码:
package Thread; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; public class Demo21 { public static void main(String[] args) throws InterruptedException { BlockingDeque<String> queue=new LinkedBlockingDeque<>(1000); //生产者线程 Thread producer=new Thread(()->{ int count=0; try { while(true){ queue.put("" +count); System.out.println("添加一个元素:"+count); count++; // Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } }); //消费者线程 Thread consumer=new Thread(()->{ try { while(true){ String element=queue.take(); System.out.println("消费一个元素:"+element); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } }); producer.start(); consumer.start(); producer.join(); consumer.join(); } }4.2 自定义阻塞队列
两个线程对一个变量修改,修改操作不是原子的,那么修改操作需要锁
注意:wait 返回之后,一定要重新检查等待条件。if只会判断一次;while被唤醒后循环重复判断,规避虚假唤醒带来的逻辑错误。
package Thread; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; public class Demo22 { static class MyBlockedQueue<T>{ private T item[]=null; private int head=0; private int tail=0; private int size=0; Object locker=new Object(); public MyBlockedQueue(int capacity){ item=(T[])new Object[capacity]; } public void put(T elem) throws InterruptedException{ synchronized(locker){ while (size >= item.length) { locker.wait(); } item[tail] = elem; tail = (tail + 1) % item.length; size++; locker.notify(); } } public T take() throws InterruptedException{ synchronized(locker){ while (size <= 0) { locker.wait(); } T elem = item[head]; head = (head + 1) % item.length; size--; locker.notify(); return elem; } } } public static void main(String[] args) throws InterruptedException { MyBlockedQueue<String> queue=new MyBlockedQueue<String>(1000); Thread profucer=new Thread(()->{ int count=0; while(true){ try{ queue.put("" +count); System.out.println("增加了一个元素:"+count); count++; // Thread.sleep(1000); }catch(InterruptedException e){ e.printStackTrace(); } } }); Thread consumer=new Thread(()->{ while(true){ try{ String element=queue.take(); System.out.println("消费一个元素:"+element); Thread.sleep(1000); }catch(InterruptedException e){ e.printStackTrace(); } } }); profucer.start(); consumer.start(); profucer.join(); consumer.join(); } }结果:
5. 线程池与工厂设计模式
5.1 线程池
线程池:线程的创建和销毁的频率提升且达到一定程度,此时产生的开销无法忽视,提前创建一批线程,放在池子里统一管理;任务来了直接复用池内已有线程,不反复新建、销毁线程。
线程池优点:
- 避免频繁创建销毁线程的开销:线程创建、销毁是操作系统系统调用,开销大;线程池复用已存在的线程对象。
- 便于控制并发数量:限制最大线程数,防止无限制创建线程耗尽 CPU、内存资源。
- 任务排队缓冲:多余任务存入队列,实现削峰填谷;配置拒绝策略保护系统。
- 便于统一管理:线程复用、监控、关闭,不需要手动逐个管理 Thread 对象。
为什么比手动 new Thread 快:
- 手动new Thread():向操作系统申请内核线程资源,属于系统调用,开销高。
- 线程池:线程只创建一次,循环从任务队列取任务反复执行;复用内核线程,省去反复创建销毁的系统调用开销。
线程池线程分类:
- 核心线程(正式员工):一般不会回收释放,只有线程池关闭才销毁。
- 临时线程(临时工):核心线程处理不过来时创建;压力降低空闲一段时间后就会被释放销毁。
5.2 工厂设计模式⬇️
1. 工厂模式解决什么问题
普通对象使用构造方法new创建;构造方法名固定,无法通过重载区分不同初始化语义。
例:Point 类,支持笛卡尔坐标(x,y)、极坐标(r,a),参数都是两个 double,无法依靠构造器重载区分。
class Point { public final double x; public final double y; // 构造器全部私有,外部禁止 new,只能走工厂方法 private Point(double x, double y) { this.x = x; this.y = y; } /** 静态工厂方法:直角坐标 */ public static Point cartesian(double x, double y) { return new Point(x, y); } /** 静态工厂方法:极坐标,内部做数学换算,返回直角坐标Point对象 */ public static Point polar(double r, double theta) { double x = r * Math.cos(theta); double y = r * Math.sin(theta); return new Point(x, y); } } public class Demo { public static void main(String[] args) { // 语义清晰,看方法名就知道用的什么坐标系 Point p1 = Point.cartesian(3, 4); Point p2 = Point.polar(5, Math.PI / 2); System.out.println(p1.x + "," + p1.y); System.out.println(p2.x + "," + p2.y); } }工厂模式思路:
不对外暴露构造器创建对象,提供一组静态工厂方法,不同方法名代表不同创建逻辑;方法内部调用构造器 new 对象,对外隐藏对象创建细节。
2. ThreadFactory(线程工厂接口)
public interface ThreadFactory { Thread newThread(Runnable r); }- 作用:线程池不直接new Thread(),交给工厂接口newThread()方法来生产线程对象。
- 可统一做线程初始化:设置线程名称、是否守护线程、线程优先级。
- 线程池内部需要大批量创建线程,工厂模式统一管控线程创建逻辑。
3. 拒绝策略 RejectedExecutionHandler
触发时机:核心线程全部占满,阻塞队列也满,再提交新任务,执行拒绝策略。
| 策略 | 行为 |
|---|---|
| AbortPolicy(默认) | 直接抛出RejectedExecutionException异常 |
| CallerRunsPolicy | 提交任务的调用者线程自己执行任务,不抛异常、不丢弃 |
| DiscardOldestPolicy | 丢弃队列存放最久的旧任务,尝试执行新任务 |
| DiscardPolicy | 静默丢弃当前新任务,无异常无日志提示 |
4. Executors 工具类
本质:工厂类,提供静态工厂方法,封装ThreadPoolExecutor,快速生成各类线程池。
//固定线程数,任务进无界队列 Executors.newFixedThreadPool(3); //单一线程 Executors.newSingleThreadExecutor(); //缓存线程池,按需创建线程,空闲回收 Executors.newCachedThreadPool(); //支持延迟/周期性定时任务 Executors.newScheduledThreadPool(10);生产环境不推荐直接使用 Executors
- Fixed / Single:使用无界队列,任务大量堆积会 OOM
- Cached:最大线程数为Integer.MAX_VALUE,会疯狂创建线程,导致 OOM
企业实践:直接实例化ThreadPoolExecutor完整构造方法,手动指定核心线程、最大线程、队列、线程工厂、拒绝策略,可控性更强。
5. 补充知识点
(1) Lambda 变量捕获
for 循环直接捕获循环变量i会出现数值错乱;需要拷贝局部变量int id = i;,捕获拷贝后的局部变量。
(2)队列区分
- LinkedBlockingQueue:普通阻塞队列,一端入队、一端出队
- LinkedBlockingDeque:双端阻塞队列,两端都可以入队、出队
(3) 线程池工作线程伪代码
while(true){ //队列无任务 take阻塞等待,任务到来才唤醒 Runnable task = queue.take(); task.run(); }简要背诵
- ThreadFactory:工厂接口统一创建线程,设置线程名、守护属性、优先级。
- 拒绝策略:线程 + 队列全部打满触发;抛异常、调用者执行、丢弃旧任务、丢弃新任务。
- Executors 是线程池工厂工具类,底层封装 ThreadPoolExecutor;生产不建议直接用,推荐手动构造 ThreadPoolExecutor。
- 工厂模式:将 new 对象封装到工厂方法,解决构造器重载限制,隐藏对象创建细节。
6. 自定义线程工厂
package Thread; import java.util.concurrent.LinkedBlockingQueue; class MyFixedThreadPool { //注意:队列存放的应该是任务Runnable,示例这里存放Thread属于写demo简化写法 private LinkedBlockingQueue<Thread> queue=new LinkedBlockingQueue<>(); public MyFixedThreadPool(int n){ for(int i=0;i<n;i++){ Thread thread=new Thread(()->{ while(true){ try { Thread r = queue.take(); r.run(); } catch (Exception e) { e.printStackTrace(); } } }); thread.start(); } } public void submit(Thread r){ try { queue.put(r); } catch (InterruptedException e) { e.printStackTrace(); } } } public class Demo25 { public static void main(String[] args) { MyFixedThreadPool pool=new MyFixedThreadPool(3); pool.submit(new Thread(()->{ System.out.println("任务1"); })); pool.submit(new Thread(()->{ System.out.println("任务2"); })); pool.submit(new Thread(()->{ System.out.println("任务3"); })); pool.submit(new Thread(()->{ System.out.println("任务4"); })); pool.submit(new Thread(()->{ System.out.println("任务5"); })); pool.submit(new Thread(()->{ System.out.println("任务6"); })); } }6. 定时器
1. 定时器作用
类似闹钟,任务不立即执行,延迟一段时间 / 指定时刻执行。
Java 标准库提供Timer;也可以手写定时器。
适用场景:延迟任务、周期性任务,例如 GC 的周期性工作。
2. 底层存储:优先队列 PriorityQueue
- 不使用ArrayList:任务插入无序,查找最近要执行的任务需要遍历全部,开销大。
- 使用PriorityQueue(小根堆):堆顶永远是执行时间最早的任务,直接取堆顶即可。
- 需要封装任务类:保存 Runnable任务 + 绝对时间戳(执行时刻)。
- 时间存储:存绝对时间戳,不存相对 delay。传入 delay,内部计算:当前时间 + delay得到绝对执行时刻,避免计时偏差。
3. 手写定时器两大核心问题
① 线程安全
一个线程提交任务schedule,一个线程执行任务,并发操作优先队列;必须加synchronized锁保护队列。
② 忙等(CPU 空转)错误写法
while(true) { synchronized(locker){ if(queue.isEmpty()) continue; MyTimerTask task = queue.peek(); long now = System.currentTimeMillis(); if(now < task.getTime()){ continue; // 时间未到,持续循环,CPU空转,频繁抢释放锁 } } }问题:时间未到就不停循环判断,消耗大量 CPU,还会引发线程饥饿。
解决:使用带超时时间的wait(timeout)
if (curTime < task.getTime()) { // 等待到任务执行时刻,时间差作为wait参数 locker.wait(task.getTime() - curTime); }wait(超时时间)两种唤醒场景:
- 定时等待:没有新任务,等待指定时长,时间到自动唤醒执行任务。
- 主动唤醒:schedule提交新任务,调用notify唤醒工作线程;新任务可能执行时间更早,需要重新判断堆顶。
注意:JDK 的PriorityBlockingQueue仅队列为空时阻塞,不能按时间差阻塞;手写定时器需要自己加锁 + wait 实现时间等待。
4. 核心流程梳理
- 封装任务类MyTimerTask:Runnable task + long executeTime绝对执行时间戳
- PriorityQueue<MyTimerTask>小根堆,按执行时间排序
- schedule(Runnable runnable, long delay):计算绝对执行时间,封装任务入队,notify唤醒工作线程
- 工作线程循环逻辑:
- 获取锁,读取堆顶任务
- 未到执行时间:wait(时间差)
- 时间到期:取出任务,执行run()
- 队列为空:无限 wait,等待新任务提交
简要背诵
- 定时器底层小根堆 PriorityQueue,堆顶保存最近任务,存储绝对时间戳。
- 杜绝忙等,用带超时 wait (时间差);既可定时唤醒,新增任务也可 notify 唤醒。
- PriorityBlockingQueue 仅空队列阻塞,手写定时器要自己实现 wait 时间等待。
- schedule 传入 delay,转为System.currentTimeMillis()+delay绝对时刻,防止计时偏移。
5. 自定义定时器
package Thread; import java.sql.Time; import java.util.PriorityQueue; //由于PriorityQueue默认是按优先级排序的,所以需要自定义比较器,根据时间排序 class MyTimerTask implements Comparable<MyTimerTask> { private Runnable task; private long time; public MyTimerTask(Runnable task, long delay) { this.task = task; this.time = System.currentTimeMillis() + delay; } public Runnable getTask() { return task; } public long getTime() { return time; } @Override public int compareTo(MyTimerTask o) { // 根据时间排序,先到先执行(小堆) return (int) (this.time - o.time); } } class MyTimer { Object lock = new Object(); PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(); public MyTimer() { Thread t = new Thread(() -> { while (true) { try { synchronized (lock) { if (queue.isEmpty()) { // 队列为空,等待任务 lock.wait(); } MyTimerTask peek = queue.peek(); long curTime = System.currentTimeMillis(); if (peek.getTime() > curTime) { // 时间没到,继续等待 // 注意:这里的wait兼顾两种情况: // 1.有新任务就唤醒 // 2.没有新任务,等待任务到时间 lock.wait(peek.getTime() - curTime); } else { // 时间到了,执行任务 MyTimerTask task = queue.poll(); task.getTask().run(); } } } catch (Exception e) { e.printStackTrace(); } } }); t.start(); } public void schedule(Runnable task, long delay) { synchronized (lock) { queue.offer(new MyTimerTask(task, delay)); lock.notify(); } } } public class Demo27 { public static void main(String[] args) { MyTimer timer = new MyTimer(); System.out.println("定时器启动,3秒后执行任务1"); timer.schedule(new Runnable() { @Override public void run() { System.out.println("执行任务1"); } }, 3000); System.out.println("定时器启动,5秒后执行任务2"); timer.schedule(new Runnable() { @Override public void run() { System.out.println("执行任务2"); } }, 5000); } }