当前位置: 首页 > news >正文

多线程(六) ~ 定时器与锁

۞ 多线程其他相关文章

文章目录

  • 一. 定时器Timer
    • (一) 定义
    • (二) 实现定时器MyTimer
      • (1) MyTimerTask
      • (2) MyTimer
      • (3) 验证正确性
  • 二. 锁(重点)
    • (一) 乐观锁策略 VS 悲观锁策略
    • (二) 轻量级锁 VS 重量级锁
    • (三) 自旋锁 VS 挂起等待锁
    • (四) 公平锁 VS 非公平锁
    • (五) 可重入锁 VS 不可重入锁
    • (六) 互斥锁 VS 读写锁
  • 三. synchronized
    • 一. 特性
    • 二. 锁升级
    • 三. 锁消除
    • 四. 锁粗化

一. 定时器Timer

(一) 定义

  1. 定时器这个概念在生活十分常见, 例如早上你定了三个闹钟来催你起床, 一个闹钟可能叫不醒你, 那第二天早上三个闹钟就会在对应时间段依照你定义的时间先后顺序响起, 而在软件开发中定时器也是一个很重要的概念, 你想安排一个程序什么时候运行, 等待一段时间再运行, 想让有的任务先执行等等…这就需要借助定时器, 其中最核心的方法就是schedule方法
    在这里插入图片描述
  1. 重要方法介绍: 定时器Timer最重要的方法就是void schedule(TimerTask task, long delay), 有两个参数:

①: TimerTask task: 定时器类里面安排的任务, 实现了Runnable接口, 说明需要重写 run 方法
在这里插入图片描述
②: long delay: 延迟时间, 也可以理解为任务的执行时间, 即当前时间戳+要推迟的时间 = 预定的时间
在这里插入图片描述

(二) 实现定时器MyTimer

定时器的实现分为两部分, 第一部分是MyTimerTask的实现, 第二部分是MyTimer主体的实现

(1) MyTimerTask

这里我们采用的是直接将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();}@Overridepublic int compareTo(MyTimerTask o) {return (int)(this.time - o.time);}
}

(2) MyTimer

  1. 这是定时器的主体, 里面的schedule更是关键, 这里需要申请 优先级队列, 将等待时间短的任务放到最前面
  2. 构造方法:
    ①: 创建一个线程循环来执行任务, 需要在线程中循环调取并且判断优先级队列的任务, 所以如果队列为空阻塞等待, 该判断应该是循环判断, 防止意外唤醒去peek空队列
    ②: 取出队列的MyTimerTask, 调用方法获取设定的时间来与当前系统时间比较, 如果设定时间>系统时间, 说明还没到时间, 就阻塞剩余的时间, 如果系统时间>设定时间的话那么执行其中的任务并且将MyTimerTask出队列, 因为里面可能会涉及多个线程同时判断与修改的操作, 所以在循环里面加锁保证原子性
    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();}}

(3) 验证正确性

在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, 那么锁的特性是什么?什么情况下会生效? 为什么会生效? 如果还有别的锁吗? 为什么我们偏偏推荐用这把锁呢? 这些, 都在下面慢慢展开…

  1. 特性:
    ①有互斥性: 核心,保证同一时间只有一个线程访问资源,锁的最重要最基本的特性
    ②保证可见性/防止指令重排序: 保证锁释放前对变量的修改对下一个获取锁的线程可见。通过内存屏障 实现,是正确性的基础,确保屏障前的指令不会被重排到屏障之后,屏障后的指令也不会被重排到屏障之前。
    ③可重入性: 同一线程可多次获取同一把锁
    ④公平性: 按请求顺序分配锁(公平)或允许竞争(非公平)。 ReentrantLock(true) 创建公平锁
  2. 生效场景:
    多个线程竞争同一把锁,即多个线程对同一对象进行操作时,加同一把锁,才会有互斥效果
  3. 生效原因:
    依赖操作系统的互斥量(Mutex) 来实现线程的阻塞和唤醒。这是一个重量级操作,会涉及从用户态到内核态的切换,JVM级别关键字,由JVM实现和管理。字节码中通过 monitorenter 和 monitorexit 指令实现

(一) 乐观锁策略 VS 悲观锁策略

  1. 乐观锁策略: 看名字就知道适用于局面乐观的场景, 乐观场景指的就是锁竞争不激烈, 数据基本很少涉及到修改操作用不到锁或者每次修改花费时间很少, 有大量空闲时间, 就算有多个线程同时读与写也可以第一时间拿到锁来确保修改操作的原子性
  1. 悲观锁策略: 适用于锁竞争激烈的悲观场景, 预测会长时间涉及到多个线程同时修改的操作, 必须要借助锁来确保修改操作的原子性, 这时候就这涉及到了锁的激烈竞争, 每个线程往往要很长时间才能竞争到锁, 这会使线程陷入长时间的等待过程

(二) 轻量级锁 VS 重量级锁

锁的核⼼特性 “原⼦性”, 操作系统基于 CPU 的原⼦指令, 实现了 mutex 互斥锁, • JVM 基于操作系统提供的互斥锁, 封装对应API之后定义实习了各种 “锁” (类)
在这里插入图片描述

  1. 轻量级锁: 当多个线程数身处于锁竞争不激烈乐观场景时, 我们使用到轻量级锁, 轻量级锁一般付出的代价比较小, 因为使用的是JVM已经封装好的并且优化过的锁相关的类与方法, 成本已经可以控制, 在用户态代码层面基本可以完成, 成本是可控的过程
  1. 重量级锁: 当多个线程在锁竞争激烈的悲观场景时, 我们使用到重量级锁, 重量级锁付出的代价一般较大, 这是因为重量级锁往往依赖于操作系统内核提供的mutex, 而在 多线程(三)线程安全问题与原因 这篇博客我们讲过, 内核中的线程调度往往是随机的不可预测的,内核态到用户态之间的切换会消耗大量时间, 对计算机来说就是苍海沧田, 成本太不可控, 你不知道你要付出什么代价…

(三) 自旋锁 VS 挂起等待锁

  1. 自旋锁: 典型的轻量级锁, 之前的篇章介绍锁的时候我们说到锁竞争失败后会陷入阻塞, 然后放弃CPU资源, 但是大多情况锁竞争失败后过不了多久锁就被释放了, 又是在锁竞争不激烈的情况下, 于是我们没必要放弃CPU资源(没必要陷入阻塞), 可以占着CPU资源不断地尝试获取锁,在锁释放的第一时间就可以获取锁(无缝衔接),不涉及到内核操作,也就没有线程调度,当然有时会有严重的忙等问题
  1. 挂起等待锁: 典型的重量级锁, 一般在线程占据线程时间较长, 锁的竞争比较激烈情况下使用, 操作系统会自动让线程陷入阻塞, 释放CPU资源, 从而陷入默默地挂起等待, 等待一段时间后偶尔被内核想起来又调度它了, 那么该线程会再次尝试竞争锁, 如果还是失败继续陷入挂起等待…

(四) 公平锁 VS 非公平锁

  1. 公平锁: 在计算机中公平指的是先来后到, 遵守顺序的执行, 相当于把线程按照申请锁的时间先后顺序放入队列, 每次让队首线程来得到锁与CPU资源去执行任务, 但不适用于并发执行, 效率很低, 但是每个线程都可以获取到锁,不会出现长期吃不到CPU资源导致线程饿死, 在ReentrantLock中, 传入true是公平锁
    在这里插入图片描述
  1. 非公平锁: 不公平就是概率均等,完全随机, 不按照顺序, 可以随意插队, 这就对应着操作系统内核对于线程的抢占式随机调度, 那么非公平锁就是线程对于该锁的获取是并发的, 抢占式的, 这会大大提升程序运行效率, 但是缺点是可能一些的线程长时间占用锁与资源, 导致别的线程长时间吃不到CPU资源饿死, 在ReentrantLock中, 不传参数默认是非公平锁
    在这里插入图片描述

(五) 可重入锁 VS 不可重入锁

  1. 可重入锁: 一个线程多次对一个线程加锁, 即本来外面加一层锁就够了, 结果里面又套了一层锁, 如果对任务多次加的是可重入锁, 那么只会出发触发一次加锁操作, 后续加锁的操作相当于没有生效, 这样不至于产生死锁这种严重bug, 我们也可以通过一个count变量来记录模拟实现, 每次加锁count++, 每次解锁count–, 直到count == 0, 设定只有count == 1时触发加锁操作, 只有count == 0时触发解锁操作, 可以实现可重入锁,可以抽象理解为,你手机的锁机密码,你既可以设置面容锁,也可以再设置图形锁,甚至可以再加一个指纹锁,但他们本质是一把锁,只不过换了个姿势,任意打开一个就能解锁全部。 Java中的 synchronized 与 ReentrantLock 就是可重入锁
  1. 不可重入锁: 一个线程对一个任务多次加锁后会陷入阻塞, 这就是外面一层已经加锁了, 里面一层又要加锁, 里面加锁会等待外面把锁释放掉, 陷入阻塞, 外面的锁又必须要等到里面任务完成后才能释放锁, 这就形成了死循环, 经典的左脑搏击右脑大脑痛击小脑

(六) 互斥锁 VS 读写锁

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

三. synchronized

一. 特性

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

二. 锁升级

在这里插入图片描述

1.第一阶段首先是不加锁的状态,这时的程序进行畅通无阻
2.当检测到synchronized关键字后,先升级为偏向锁: 相当于给该线程1身上打了个标记,拥有了许可证,而不是真加锁,如果到任务结束也没有线程2竞争该锁,再把这个标记删除,但程序运行中一旦有线程2尝试竞争该锁时,会先判断前面是否已经有线程打上了标记,如果有的话那么标记升级为自旋锁,也就是轻量级锁,线程2进入阻塞等待
3.当锁竞争越来越激烈,自旋锁常常会陷入长时间等待, 忙等状态会消耗大量CPU资源时候,会进一步升级为重量级锁,锁竞争失败会交由操作系统管理,陷入挂起等待,不再消耗CPU资源,什么时候唤醒尝试重新竞争锁由操作系统决定
4.需要注意:JVM只提供了锁升级而没有锁降级,意味着只要变成升级为下一阶段的重量级锁时,就不会再变成原来的轻量级锁

三. 锁消除

编译器的一种优化机制,当代码编译为.class文件时,编译器会先检查你的代码,如果发现有的地方不涉及到线程安全确实不用加锁但你又确实写了加锁语句时,编译器会自动把加锁语句去除,以提高性能,当然编译器有百分百把握时才会对你的加锁语句改动,也不用担心会出现优化而导致的线程安全(编译机制并不是万能的,有时候可以多检查一下代码,有没有不必要的加锁条件,事在人为)

四. 锁粗化

1.了解锁粗化之前要知道锁的粗与细是根据什么区分的?
粒度
2.什么是粒度?
锁的粒度是按照加锁与解锁之间所包含的指令与执行时间划分的,涉及的指令越多,执行时间越长,那么锁的粒度越大,锁就越粗
3.锁的粗化:
当针对第一段代码的每个部分进行细粒度的加锁时,会因为锁的多次获取与释放而严重影响性能,这时编译器可以把这一段代码的所有部分的细粒度加锁优化成一次加锁,即锁的粗化,这时候锁只需要获取和释放一次就可以达到多次细粒度加锁的效果,从而提升性能(当然也不是锁越粗越好,加锁就意味着强行进入了串行执行的状态,这时候本来一些可以并发实行的任务也会被迫串行化执行)


文章转载自:

http://PYpdWKBB.Lfdzr.cn
http://NK0v2P76.Lfdzr.cn
http://rvHSQxMw.Lfdzr.cn
http://ZWYUf3C9.Lfdzr.cn
http://kiLkLuXZ.Lfdzr.cn
http://ByIPGhRx.Lfdzr.cn
http://J8zSBKuo.Lfdzr.cn
http://mwhlVI9C.Lfdzr.cn
http://s8LMsyN1.Lfdzr.cn
http://T759FvTA.Lfdzr.cn
http://U9tSglKx.Lfdzr.cn
http://00u54oQ7.Lfdzr.cn
http://sHwMHPer.Lfdzr.cn
http://5mxb5zPq.Lfdzr.cn
http://GJIuEoRl.Lfdzr.cn
http://DFAKwIb3.Lfdzr.cn
http://IaTETXCQ.Lfdzr.cn
http://CZRTOzN8.Lfdzr.cn
http://tlnyhp3p.Lfdzr.cn
http://pkSM19TS.Lfdzr.cn
http://N3JEznUX.Lfdzr.cn
http://CdSlnwUu.Lfdzr.cn
http://pcWJZIiC.Lfdzr.cn
http://ZNCBwdua.Lfdzr.cn
http://bSxhQvt3.Lfdzr.cn
http://BBwFwJLM.Lfdzr.cn
http://09yaCvSn.Lfdzr.cn
http://WVdBV94S.Lfdzr.cn
http://QN2GPlZP.Lfdzr.cn
http://lGnAk8v0.Lfdzr.cn
http://www.dtcms.com/a/370478.html

相关文章:

  • OpenSSL 1.0.1e 下载解压和运行方法(小白适用 附安装包)​
  • Qt图表功能学习
  • 【营销策略算法】关联规则学习-购物篮分析
  • 部署AIRI
  • 深度学习基础概念回顾(Pytorch架构)
  • 基于LSTM深度学习的网络流量测量算法matlab仿真
  • 【PyTorch实战:Tensor变形】5、 PyTorch Tensor指南:从基础操作到Autograd与GPU加速实战
  • 【基础-判断】@Entry装饰的自定义组件将作为页面的入口。在单个页面中可以使用多个@Entry装饰不同自定义组件。
  • 驱动开发系列71 - GLSL编译器实现 - 指令选择
  • 贪心算法应用:化工反应器调度问题详解
  • OpenAvatarChat项目在Windows本地运行指南
  • canal+DataX实现数据全量/实时同步
  • Jenkins运维之路(自动获得分支tag自动构建)
  • 服务器内存和普通计算机内存在技术方面有什么区别?
  • 同一台nginx中配置多个前端项目的三种方式
  • 【LeetCode热题100道笔记】排序链表
  • Shell 脚本实现系统监控与告警
  • 【算法--链表】86.分割链表--通俗讲解
  • 基于区块链的IoMT跨医院认证系统:Python实践分析
  • 用内存顺序实现 三种内存顺序模型
  • rh134第五章复习总结
  • Java包装类型
  • Linux awk 命令使用说明
  • 一个正常的 CSDN 博客账号,需要做哪些基础准备?
  • 文件I/O与I/O多路复用
  • protobuf的序列反序列化
  • Linux/UNIX系统编程手册笔记:共享库、进程间通信、管道和FIFO、内存映射以及虚拟内存操作
  • 吴恩达机器学习(九)
  • 基于多级特征编码器用于声学信号故障检测模型
  • 【LeetCode热题100道笔记】二叉树中的最大路径和