多线程(定时器的模拟实现,锁策略)5
目录
一.定时器的模拟实现
1.Timer的简单使用
Timer是Java标准库中最基础的定时任务工具,适合简单的定时任务调度。
这里的Timer和线程池一样,其中也包含了前台进程,阻止进程结束。
下面采用匿名内部类的写法,实现Timer
public class Demo11 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask(){
@Override
public void run() {
System.out.println("hello 3000");
}
},3000);//等待3秒
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 2000");
}
},2000);//等待2秒
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 1000");
}
},1000);//等待1秒
System.out.println("hello main");
}
}
由于赋值给schedule,所有主程序先执行,后面根据等待秒数依次执行

2.模拟实现TimerTask
模拟实现思路:
1.创建一个类,表示一个任务
2.将定时器中,能够管理多个任务的,使用一个集合类管理起来(采用优先级队列)。
3.实现schseule方法,把任务添加到队列中。
4.额外创建一个线程,负责执行队列中的任务。(这里和线程池不同,线程池只需要看队列不为 空,就可以立刻执行,而这里则需要看时间,时间到,执行,时间没到,不执行)。
import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;
class MyTimerTask implements Comparable<MyTimerTask> {//实现 Comparable 接口,目的是让任务之间能够比较大小,从而在优先队列中自动排序
private Runnable task;//实际执行时间
private long time;
public 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 Object object = new Object();
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();//创建一个优先队列(小根堆)
//实现schedule方法
private void schedule(Runnable task, long delay) {
synchronized(object){
MyTimerTask timeTask = new MyTimerTask(task, System.currentTimeMillis() + delay);
//System.currentTimeMillis() 为获取当前时间戳
queue.offer(timeTask);//
object.notify();//唤醒后台线程。因为可能新加入的任务比当前堆顶任务更早,需要重新计算等待时间
}
}
public MyTimer() {
synchronized (object) {
Thread t = new Thread(() -> {
try {
while (true) {
while (queue.isEmpty()) {//如果队列为空,就释放锁并阻塞等待,直到有人调用 schedule() 添加任务并 notify() 唤醒
object.wait();
}
MyTimerTask task = queue.peek();
if (System.currentTimeMillis() < task.getTime()) {//计算时间差
object.wait(task.getTime() - System.currentTimeMillis());
} else {
task.run();//执行任务
queue.poll();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
}
}
}
public class Demo12 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask(){
@Override
public void run() {
System.out.println("hello 3000");
}
},3000);
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");
}
}

定时器的优点:提高效率,精准控制,资源优化,任务调度灵活。
定时器的缺点:依赖系统稳定性,调试复杂,资源竞争风险,时间同步问题。
二.常见的锁策略
1.悲观锁和乐观锁
悲观锁和乐观锁描述的是加锁时候的场景
悲观锁:在加锁的时候,预测接下来发生的锁竞争非常激烈,此时就需要对这样激烈的情况做一些额外的工作。
eg:有一把锁,有20个线程同时竞争,每一个线程加锁的频率都很高,此时一个线程加锁,大概率被另一个线程所占用
乐观锁:在加锁的时候,预测接下来发生的锁竞争不激烈,此时就需要不需要做一些额外的工作。
eg:有一把锁,有2个线程同时竞争,每一个线程加锁的频率都很高,此时一个线程加锁,大概率不会被另一个线程所占用
2.重量级锁和轻量级锁
重量级锁和轻量级锁是遇到场景之后的解决方案
重量级锁:在悲观锁的场景下,此时就要付出更多的代价 --------> 更低效
轻量级锁:在乐观锁的场景下,此时付出的代价会更小 ---------> 更高效
3.挂起等待锁和自旋锁
挂起等待锁:是重量级锁的典型实现,是操作系统内核级别的,在加锁的时候如果发生竞争,此时该线程就会进行阻塞状态,后续就需要内核进行唤醒。
它获取锁的周期更长,很难做到及时获取,这个过程不消耗cpu。
自旋锁:是轻量级锁的典型实现,是应用程序级别的,一般不会进行阻塞状态,而是进入忙等
它获取锁定周期更短,可以及时获取到锁,但是这个过程会一直消耗cpu。
4.普通互斥锁和读写锁
普通互斥锁:互斥锁是最简单的锁类型,保证同一时间只有一个线程能访问共享资源(java中常用的就是synchronized)。
读写锁:读写锁区分读操作和写操作,允许多个线程并发读,但写操作独占资源。
在服务开发中一般都是读大于写,因此不能给读操作和写操作都加上锁,这样的话会造成所冲突,因此读写锁是确保读锁和读锁之间不互斥(不会产生阻塞),只让读锁和写锁,写锁和写锁之间产生互斥。
5.可重入锁和不可重入锁
可重入锁:可重入锁允许同一个线程多次获取同一把锁,内部通过计数器记录重入次数。每次获取锁时计数器加1,释放锁时计数器减1,直到计数器归零才真正释放锁。
不可重入锁:不可重入锁不允许同一线程重复获取锁,若线程尝试重复获取,会导致死锁或阻塞。
核心要点:
1.锁要记录当前是哪个线程拿到的这把锁。
2.使用计数器,记录当前加锁了多少次,在合适的位置解锁。
6.公平锁和非公平锁
公平锁:线程获取锁的顺序严格按照请求顺序执行,保证等待时间最长的线程优先获得锁。
非公平锁:允许插队,新请求的线程可能直接获取锁,无需考虑等待队列中的顺序。
三.锁的自适应过程
无锁 ——> 偏向锁 ——>自旋锁 ——> 重量级锁
偏向锁的过程:进行synchronized ,刚一上来不是真正的加锁,而是简单标记一下(这个标记很轻,相对于解锁来说,效率要高很多),如果没有其他所前来竞争,最终也就是清除标记,如果有其他线程来竞争,那么就会抢先一步执行。
无锁 ——> 偏向锁:代码进入synchronized的代码块
偏向锁 ——>自旋锁 :发现有其他线程竞争
自旋锁 ——> 重量级锁:发现竞争激烈
四.锁消除
锁消除是一种编译器优化技术,主要用于消除不必要的同步操作。编译器会判定是否真的需要加锁,如果确实不需要,但是有写了synchronized,就会把synchronized给去掉。
优点:
减少不必要的同步开销,提升程序性能。
无需修改代码即可由编译器自动优化。
缺点:
依赖编译器的逃逸分析能力,优化效果因场景而异。
无法消除真正需要同步的场景。
五.锁粗化
锁的粒度:加锁与解锁之间,包含的代码越多,就认为锁的粒度越粗,反之就认为锁的粒度越细。
锁粗化就是将搬来执行多次加锁的操作,优化成一次加锁解锁~~
synchronized (obj) {
// 操作1
}
synchronized (obj) {
// 操作2
}
// 可能被优化为:
synchronized (obj) {
// 操作1 + 操作2
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)