
①: TimerTask task: 定时器类里面安排的任务, 实现了Runnable接口, 说明需要重写 run 方法

②: long delay: 延迟时间, 也可以理解为任务的执行时间, 即当前时间戳+要推迟的时间 = 预定的时间

定时器的实现分为两部分, 第一部分是MyTimerTask的实现, 第二部分是MyTimer主体的实现
这里我们采用的是直接将Runnable作为成员属性, 不再实现其Runnable接口, 一样可以达到作为任务的功能, 我们将任务Runnable task 与 时间long time 作为MyTimerTask的构造方法参数, 在MyTimerTask中来计算总等待时间, 因为我们想要的是等待时间短的任务放到最前面, 所以需要申请优先级队列实现小根堆, 所以这里需要实现Comparable接口
/**
* Created with IntelliJ IDEA.
* Description:
* User: ran
* Date: 2025-08-04
* Time: 20:36
*/
class MyTimerTask implements Comparable<MyTimerTask>{
private Runnable task;
private long time;
public MyTimerTask(Runnable task, long time) {
this.task = task;
this.time = time;
}
public Runnable getTask() {
return task;
}
public long getTime() {
return time;
}
public void run() {
task.run();
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time - o.time);
}
} public MyTimer() {
// 创建线程来执行任务
Thread thread = new Thread(() -> {
try {
while (true) {
synchronized (lock) {
while (priorityQueue.isEmpty()) {
lock.wait();
}
MyTimerTask task = priorityQueue.peek();
if (task.getTime() > System.currentTimeMillis()) {
lock.wait(task.getTime() - System.currentTimeMillis());
} else {
task.run();
priorityQueue.poll();
}
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread.start();
}3, schedule方法, 设置参数(Runnable task, long delay), 创建一个 MyTimerTask 对象, 将任务与设定时间传进去, 加入队列后唤醒构造方法中因为队列空而执行的wait, 因为整个操作涉及到判断与写的操作, 要加锁确保原子性
public void schedule(Runnable task, long delay) {
synchronized (lock) {
MyTimerTask timerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);
priorityQueue.offer(timerTask);
lock.notify();
}
}在main方法定义测试用例

package test;
import java.util.PriorityQueue;
import java.util.Timer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created with IntelliJ IDEA.
* Description:
* User: ran
* Date: 2025-08-04
* Time: 20:36
*/
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(() -> {
System.out.println(Thread.currentThread() + " 定时1s 后执行");
}, 1000);
timer.schedule(() -> {
System.out.println(Thread.currentThread() + " 定时2s 后执行");
}, 2000);
timer.schedule(() -> {
System.out.println(Thread.currentThread() + " 定时3s 后执行");
}, 3000);
timer.schedule(() -> {
System.out.println(Thread.currentThread() + " 定时4s 后执行");
}, 4000);
timer.schedule(() -> {
System.out.println(Thread.currentThread() + " 定时5s 后执行");
}, 5000);
timer.schedule(() -> {
System.out.println(Thread.currentThread() + " 定时6s 后执行");
}, 6000);
}谈到锁相信大家已经不陌生了, 定时器与阻塞队列的实现我们都用到了锁synchronized, 那么锁的特性是什么?什么情况下会生效? 为什么会生效? 如果还有别的锁吗? 为什么我们偏偏推荐用这把锁呢? 这些, 都在下面慢慢展开…
锁的核⼼特性 “原⼦性”, 操作系统基于 CPU 的原⼦指令, 实现了 mutex 互斥锁, • JVM 基于操作系统提供的互斥锁, 封装对应API之后定义实习了各种 “锁” (类)



互斥锁: 上面我们讲过这是锁的最基本也是最重要的特性,可以说是个锁就能互斥 读写锁: 1. 多个线程操作同一个数据并不一定全都有线程安全,例如多线程对一个数据的读操作就天然线程安全,再怎么读该数据也不会发生改变。因此,涉及到多个线程对同一数据进行修改时,就涉及到了线程安全问题(具体有什么线程安全问题见博客-- 线程问题安全与解决) 2. 节省一些不必要的锁开销,可以提高程序的性能,尤其在读多写少的情况下 ①:多线程的对同一数据的读与读之间不涉及线程安全,不需要互斥 ②:多线程的对同一数据的读与写需要互斥 ③:多线程的对同一数据的写于写之间需要互斥 3.①:读锁 ReentrantReadWriteLock.ReadLock

②:写锁 ReentrantReadWriteLock.WriteLock

| synchronized |
|:----|
| 自适应锁,意思是可以根据锁的具体竞争激烈程度由轻量级锁转重量级锁,既适用于乐观场景又适用于悲观场景 |
| 非公平锁,根据内核随机调度来抢占式进行加锁,所有线程竞争该锁的概率均等 |
| 可重入锁,可以对一个线程进行多次加锁而不陷入死锁 |
| 不是读写锁,对于读与写一视平等,统统互斥 |

1.第一阶段首先是不加锁的状态,这时的程序进行畅通无阻 2.当检测到synchronized关键字后,先升级为偏向锁: 相当于给该线程1身上打了个标记,拥有了许可证,而不是真加锁,如果到任务结束也没有线程2竞争该锁,再把这个标记删除,但程序运行中一旦有线程2尝试竞争该锁时,会先判断前面是否已经有线程打上了标记,如果有的话那么标记升级为自旋锁,也就是轻量级锁,线程2进入阻塞等待 3.当锁竞争越来越激烈,自旋锁常常会陷入长时间等待, 忙等状态会消耗大量CPU资源时候,会进一步升级为重量级锁,锁竞争失败会交由操作系统管理,陷入挂起等待,不再消耗CPU资源,什么时候唤醒尝试重新竞争锁由操作系统决定 4.需要注意:JVM只提供了锁升级而没有锁降级,意味着只要变成升级为下一阶段的重量级锁时,就不会再变成原来的轻量级锁
编译器的一种优化机制,当代码编译为.class文件时,编译器会先检查你的代码,如果发现有的地方不涉及到线程安全确实不用加锁但你又确实写了加锁语句时,编译器会自动把加锁语句去除,以提高性能,当然编译器有百分百把握时才会对你的加锁语句改动,也不用担心会出现优化而导致的线程安全(编译机制并不是万能的,有时候可以多检查一下代码,有没有不必要的加锁条件,事在人为)
1.了解锁粗化之前要知道锁的粗与细是根据什么区分的?
粒度
2.什么是粒度?
锁的粒度是按照加锁与解锁之间所包含的指令与执行时间划分的,涉及的指令越多,执行时间越长,那么锁的粒度越大,锁就越粗
3.锁的粗化:
当针对第一段代码的每个部分进行细粒度的加锁时,会因为锁的多次获取与释放而严重影响性能,这时编译器可以把这一段代码的所有部分的细粒度加锁优化成一次加锁,即锁的粗化,这时候锁只需要获取和释放一次就可以达到多次细粒度加锁的效果,从而提升性能(当然也不是锁越粗越好,加锁就意味着强行进入了串行执行的状态,这时候本来一些可以并发实行的任务也会被迫串行化执行)