【多线程5】面试常考锁知识点

文章目录

  • 悲观/乐观锁
  • 挂起等待锁/自旋锁
  • 偏向锁
  • 轻量级/重量级锁
  • 锁升级
  • CAS
    • CAS引发的ABA问题
      • 解决方案
    • 原子类
  • 公平/不公平锁
  • 可重入锁
    • ReentrantLock
        • 读写锁
  • Callable接口

这里的“悲观”“乐观”“挂起等待”“自旋”“轻量级”“重量级”“公平”“非公平”“可重入”仅代表某个锁的特性,不具体指某个锁。

悲观/乐观锁

悲观锁:总是在考虑最坏的情况,认为在拿数据时总是存在线程不安全问题,总是认为别人在同时修改这个数据,于是每次拿的时候给这个数据上锁,让其他线程拿不了直到锁释放。代表有synchronized、ReentrantLock。(重量级锁有悲观锁的影子!)
乐观:总是考虑最好的情况,认为总是不存在有线程在同时修改的情况,放心的去拿数据,但是乐观锁有CAS的机制,如果要进行更新操作,发现读取到更新之间有其他线程修改了这个数据,则会放弃修改,重新读取再修改直到成功。代表有原子类,原子类底层就是不断的CAS。(轻量级锁、偏向锁都有乐观锁的影子!)

挂起等待锁/自旋锁

自旋锁:线程未获得锁则不断尝试获取锁。可以及时获得锁,CPU空转(忙等),但缺点是可能会很消耗CPU资源,因此自旋到一定条件(设定时间、次数)就不自旋了。

while(抢锁失败){;if(!自旋条件) break;
}

挂起等待锁:线程未获得锁则进入阻塞状态,等待被唤醒。不会很消耗CPU资源,但缺点是等待周期可能会很长,不能及时获得锁。

偏向锁

当同步代码块只有一个线程运行时(可以理解为只有一个线程参与锁竞争),此时并不会给这个线程加锁,而是给这个线程一个标记ID(由JVM记录,这个机制也实现了Java的可重入锁,方便理解可参考我写的解决死锁段落),当下一次进入同步代码块时,根据这个ID判断是不是仍然还是这个线程,如果是则直接运行,否则偏向锁升级为轻量级锁,锁持有者为获取锁的线程,其他线程自旋。
上面的过程有点抽象,总结来说:只有一个线程执行同步代码块的时候,此刻加的锁为偏向锁,如果发生其他线程(同时竞争不激烈)来竞争锁,则锁升级为轻量级锁,如果竞争再激烈,则升级为重量级锁。

轻量级/重量级锁

轻量级锁:锁竞争不激烈的场景下,线程未获得锁则保持自旋(不阻塞等待一直尝试获取锁)。
重量级锁:锁竞争激烈的场景下,线程未竞到锁则进入阻塞状态,等待被唤醒,典型:synchronized

锁升级

Java的synchronized有锁升级的机制:

在这里插入图片描述
synchronized自适应进行升级的过程,保证了JVM不盲目加锁浪费资源,在锁竞争缓和的情况下线程不阻塞浪费时间,及时获取到锁,在锁竞争激烈的情况下,让线程阻塞减轻CPU负担。

CAS

CAS(Compare And Swap)。顾名思义先比较再交换,CAS涉及到三个数据,这里可以理解为value(修改前读取到的值;主内存中的共享变量)、exceptValue(上一次修改后保存的值;线程里的局部变量)、swapValue(要修改的值;线程里的局部变量)。定义为exceptValue是因为CAS期待exceptValue修改为swapValue。
CAS的工作机制:

  • 读取内存值(value)。
  • 比较 value 和exceptValue。
  • 如果相等,则写入 swapValue,否则CAS失败。

这三步操作是不可分割的也就是说一次CAS是原子性的
exceptValue的值在CAS成功后被更新为value,否则保持原值不变。

CAS引发的ABA问题

假设原值为A,要修改为B。如果存在多个线程要执行这个修改操作:

  • 一个线程修改成功后,由于缓存一致性协议,value变化时,其他线程已经读取的value也会被强制刷新为最新值。多个线程进行CAS(A,A,B)时,一个成功后,其他的线程会CAS失败,不会对A重复CAS(A,A,B)。
  • 但是CAS只检查值,不关心在修改这个值之前是否发生了A->B->…->A这种骚操作。由此引发了ABA问题。例如:

线程t1要将A修改为B,而线程t2要将A修改为B,再修改为A;
如果t2先执行完这两个CAS操作,t1由于缓存一致性协议会实时读取到最新值,所以t1在CAS前里面的A变为B再变为A,t1执行CAS,两次线程结束后最终结果为B。

虽然上面这ABA操作看起来没问题,但极端情况下却容易出问题:

我有200块存款,要取100块钱;
我来到ATM,插卡输密码,设置取100块再点击确定,恰好系统超时没有及时吐钱,我恼怒的再摁了一下,由此产生了两个线程t1,t2。两个线程都要CAS(200,200,100),正常情况下,总有一个成功而另一个失败,但是又恰好老妈打给我100块生活费,产生线程t3,在t2完成CAS后,t3又来一波CAS(100,100,200)加了100块,没有t3,t1应该是CAS(100,200,100)但现在成了CAS(200,200,100),于是ATM再吐100块。
这是不符合实际的,现实生活即便手痒快速摁几下,也不会重复响应。

解决方案

为此基于CAS只比较新旧值的特性引入了版本号,版本号是一个单调递增的常量,每次CAS就+1,这样即便A->B->A也能因为版本号而察觉到。

原子类

针对多线程操作共享变量例如例如变量自增这种而不发生线程不安全问题,可以使用原子类。原子类底层使用了CAS,可以保证变量操作的原子性,常见的原子类有AtomicInteger、AtomicIntegerArray、AtomicBoolean、AtomicLong、AtomicReference、AtomicStampedReference。举例AtomicInteger:

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;public class Main {public static void main(String[] args) {AtomicInteger i = new AtomicInteger();i.addAndGet(1); //对于int/Integer i,相当于i+=1System.out.println(i);i.decrementAndGet();//相当于--iSystem.out.println(i);i.getAndDecrement();//相当于i--System.out.println(i);i.incrementAndGet();//相当于++iSystem.out.println(i);i.getAndIncrement();//相当于i++System.out.println(i);}
}

公平/不公平锁

Java对公平锁的定义是先来后到,哪个线程先抢锁就哪个线程拿锁,所以非公平锁就是所有锁竞争者获得锁的机会均等(操作系统随机调度线程)。

可重入锁

一个线程执行到同步代码块给自己加了锁,在这个代码块里又给自己加了锁,造成了死锁,这个线程就自己阻塞自己,锁也释放不了。如果允许线程给自己多次加锁,但又不会发生死锁,同步代码块执行完后锁正常释放,那就是可重入锁

学Java的同学不用担心死锁的问题,只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

ReentrantLock

和synchronized同级别,是在Java5以后引入的,相比于synchronized对锁的使用更灵活,也更复杂。以下是同synchronized的对比:

synchronizedReentrantLock
实现方式synchronzied是关键字,实现机制在JVM内部,需要手动解锁ReentrantLock是Java.util.concurrent.locks.ReentrantLock里的类,是在JVM外部实现的,而ReentrantLock需要手动unlock()解锁
锁竞争处理线程锁竞争失败会一直阻塞不会阻塞;可以通过trylock(TIME)/trylock()返回一个boolean值,如果是false代表竞争锁失败,调用者可根据这个判断编写竞争失败的代码逻辑
锁特性非公平锁默认非公平锁,可通过构造方法传入ture成为公平锁
等待和唤醒可通过Object类的wait()、notify()进行等待和唤醒,但唤醒目标是随机的可通过Condition类指定唤醒某个线程

常用方法lock()trylock(Time)unclock()

import java.util.concurrent.locks.ReentrantLock;public class Main {public static void main(String[] args) {ReentrantLock locker=new ReentrantLock();Thread t1=new Thread(()->{for(int i=0; i<500; i++){try{locker.lock();System.out.println(Thread .currentThread().getName()+i);}finally{locker.unlock();//未防止忘记释放锁,将其放在finally里}}});t1.start();}
}
读写锁

多个线程对共享变量的读取是不会发生线程不安全的的,如果有写入的情况才会发生:

  1. 所有线程只读,线程安全
  2. 所有线程写入,线程不安全
  3. 有读有写,线程不安全

当有多个线程操作一个共享变量时,对其加锁且对它的读取是不互斥,但是写入时只能一个线程持有锁,达到:

  1. 读与读之间不互斥
  2. 所有线程写入,一个线程持有锁其他阻塞
  3. 有读有写,一个线程持有锁其他阻塞

于是我们引入了读写锁ReentrantReadWriteLock类,通过ReentrantReadWriteLock.readLock获取一个读锁ReadLock对象;通过ReentrantReadWriteLock.writeLock获取一个写锁WriteLock对象。
这两个对象使用lock()tryLock()、**unlock()**来加锁释放锁。

import java.util.concurrent.locks.ReentrantReadWriteLock;public class Main {static int i=0;public static void main(String[] args) {ReentrantReadWriteLock locker=new ReentrantReadWriteLock();Thread t1=new Thread(()->{locker.readLock().lock();//readLock是ReentrantReadWriteLock类里RreadLock类型的字段System.out.println(i);locker.readLock().unlock();locker.writeLock().lock();//writeLock是ReentrantReadWriteLock类里WriteLock类型的字段i+=1;locker.writeLock().unlock();});t1.start();}
}

Callable接口

Callable类似于Runnable接口,里面定义了call()方法,同run()方法一样对任务进行了包装。
但不同的是Callable接口带有泛型,且call()方法带有返回值,且不可直接将实现了Callable接口的类对象作为参数直接传入Thread类的构造方法里,需要通过FutureTask类将任务提交到线程里。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Main {public static void main(String[] args) throws InterruptedException, ExecutionException {//返回一个实现了Callable接口的匿名类对象Callable<Integer> callable = (() -> {int sum = 0;for (int i = 1; i <= 100; i++) {sum += i;}return sum;});FutureTask<Integer> futureTask = new FutureTask<>(callable);//FutureTask实现了RunnableFuture接口,RunnableFuture继承了Runnable接口Thread t = new Thread(futureTask);t.start();System.out.println(futureTask.get());}
}

如果要实现上面同样的功能,还需在Main里定义一个static字段类记录sum的值方便任务执行完后来确定任务效果。而通过call()带返回值的属性和new FutureTask.get()方法得到任务结果,将任务和线程分开,达到高内聚低耦合的目的。

完。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/902127.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第三届世界科学智能大赛新能源赛道:新能源发电功率预测-数据处理心得体会1

看懂数据 比赛数据说明&#xff1a; 文档&#xff08;报名之后可以下载&#xff09;大小操作初赛测试集.zip94MB下载初赛训练集.zip632MB下载output.zip145KB下载 任务和主题 AI新能源功率预报&#xff1a;根据历史发电功率数据和对应时段多类别气象预测数据&#xff0c;实…

【云馨AI-大模型】2025年4月第三周AI领域全景观察:硬件革命、生态博弈与国产化突围

一、硬件算力突破点燃多智能体时代 谷歌在4月12日Cloud Next大会发布第七代TPU Ironwood&#xff0c;单芯片算力达4614 TFLOPs&#xff0c;较前代内存提升6倍&#xff0c;专为AI推理场景优化。配合发布的Gemini 2.5 Flash模型通过"思考"功能实现成本优化&#xff0c…

第3章 垃圾收集器与内存分配策略《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》

第3章 垃圾收集器与内存分配策略 3.2 对象已死 Java世界中的所有对象实例&#xff0c;垃圾收集器进行回收前就是确定对象哪些是活着的&#xff0c;哪些已经死去。 3.2.1 引用计数算法 常见的回答是&#xff1a;给对象中添加一个引用计数器&#xff0c;有地方引用&#xff0…

超详细VMware虚拟机扩容磁盘容量-无坑版

1.环境&#xff1a; 虚拟机&#xff1a;VMware Workstation 17 Pro-17.5.2 Linux系统&#xff1a;Ubuntu 22.04 LTS 2.硬盘容量 虚拟机当前硬盘容量180G -> 扩展至 300G 3.操作步骤 &#xff08;1&#xff09;在虚拟机关机的状态下&#xff0c;虚拟机硬盘扩容之前必…

HarmonyOS:1.4 - HarmonyOS应用程序框架基础

判断题 1.在基于Stage模型开发的应用项目中都存在一个app.json5配置文件、以及一个或多个module.json5配置文件。 正确(True) 2.一个应用只可以包含一个UIAbility组件。 错误(False) 3.Background状态在UIAbility实例销毁时触发。可以在onDestroy()回调中进行系统资源的释…

HTTP HTTPS RSA

推荐阅读 小林coding HTTP篇 文章目录 HTTP 80HTTP 响应码1xx&#xff1a;信息性状态码&#xff08;Informational&#xff09;2xx&#xff1a;成功状态码&#xff08;Success&#xff09;3xx&#xff1a;重定向状态码&#xff08;Redirection&#xff09;4xx&#xff1a;客户端…

ORACLE数据库转国产阿里OceanBase数据库

1.BLOB类型修改 将接口内oracle.sql.BLOB改为java.sql.Blob 2.REGEXP_LIKE 判断函数正则表达式中字符转义问题 OB的正则表达式使用的是标准的Linux模式,oracle是黑盒子,在处理部分转义符([])的时候, Oracle无需使用转义符,OB务必使用转义符,加/转义处理,例如在regexp_like(t…

STM32的三种启动方式

目录 一、从主闪存存储器启动&#xff08;Main Flash Memory&#xff09; 二、从系统存储器启动&#xff08;System Memory&#xff09; 三、从内置SRAM启动&#xff08;Embedded SRAM&#xff09; 一、从主闪存存储器启动&#xff08;Main Flash Memory&#xff09; >&g…

Flutter使用flutter_driver进行自动化测试

Flutter自动化测试实践指南 作为一名iOS开发者&#xff0c;我最近对Flutter的自动化测试产生了浓厚兴趣。在开发过程中&#xff0c;我发现自动化测试对于保证应用质量至关重要&#xff0c;特别是像我们这样的创业团队&#xff0c;测试资源有限的情况下。 搭建Flutter自动化测…

Halcon应用:九点标定-手眼标定

提示&#xff1a;若没有查找的算子&#xff0c;可以评论区留言&#xff0c;会尽快更新 Halcon应用&#xff1a;九点标定-手眼标定 前言一、Halcon应用&#xff1f;二、应用实战1、图形理解[eye-to-hand]&#xff1a;1.1、开始应用2、 图形理解[eye-in-hand]2.1、 开始应用 前言…

【C++11】列表初始化、右值引用、完美转发、lambda表达式

&#x1f4da; 博主的专栏 &#x1f427; Linux | &#x1f5a5;️ C | &#x1f4ca; 数据结构 | &#x1f4a1;C 算法 | &#x1f310; C 语言 上篇文章&#xff1a;unordered_map、unordered_set底层编写 下篇文章&#xff1a;C11&#xff1a;新的类功能、模板的可…

Pandas取代Excel?

有人在知乎上提问&#xff1a;为什么大公司不用pandas取代excel&#xff1f; 而且列出了几个理由&#xff1a;Pandas功能比Excel强大&#xff0c;运行速度更快&#xff0c;Excel除了简单和可视化界面外&#xff0c;没有其他更多的优势。 有个可怕的现实是&#xff0c;对比Exce…

Vue 3 中将 ref 创建的响应式对象数据转换为普通(非响应式)的数据

Vue 3 中使用 ref 创建的响应式对象数据转换为普通&#xff08;非响应式&#xff09;的数据&#xff0c;有以下几种方法&#xff1a; 1. 访问 .value 属性: 这是最直接、最常见的方法。 由于 ref 对象的值存储在其 .value 属性中&#xff0c;直接访问该属性即可获得普通数据。…

四月下旬系列

CUHKSZ 校赛 期中考试 DAY -1。 省流&#xff1a;前 1h 切 6 题&#xff0c;后 3h 过 1 题&#xff0c;读错一个本来很【】的题&#xff0c;被大模拟构造创【】了。 本地除了 VSCode 没有 Extensions&#xff0c;别的和省选差不多。使用 DEVC。 前 6 题难度 < 绿&#x…

下采样(Downsampling)

目录 1. 下采样的定义与作用​​ ​​2. 常见下采样方法​​ ​​(1) 池化&#xff08;Pooling&#xff09;​​ ​​(2) 跨步卷积&#xff08;Strided Convolution&#xff09;​​ ​​(3) 空间金字塔池化&#xff08;SPP&#xff09;​​ ​​3. PyTorch 实现示例​​ …

lottie深入玩法

A、json文件和图片资源分开 delete 是json资源名字 /res/lottie/delete_anim_images是图片资源文件夹路径 JSON 中引用的图片名&#xff0c;必须与实际图片文件名一致 B、json文件和图片资源分开&#xff0c;并且图片加载不固定 比如我有7张图片&#xff0c;分别命名1~7&…

高精度算法(加、减、乘、除、阶乘和)​

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 唯有主动付出&#xff0c;才有丰富的果…

探索大语言模型(LLM):马尔可夫链——从诗歌分析到人工智能的数学工具

提出背景与灵感起源 马尔可夫链由俄国数学家安德雷马尔可夫于1906年提出&#xff0c;最初是为了挑战当时概率论中“独立性假设”的局限性。他希望通过研究相依变量序列&#xff0c;证明即使随机变量之间存在依赖关系&#xff0c;大数定律和中心极限定理仍然成立。 灵感来源&am…

【web服务_负载均衡Nginx】三、Nginx 实践应用与高级配置技巧

一、Nginx 在 Web 服务器场景中的深度应用​ 1.1 静态网站部署与优化​ 在 CentOS 7 系统中&#xff0c;使用 Nginx 部署静态网站是最基础也最常见的应用场景。首先&#xff0c;准备网站文件&#xff0c;在/var/www/html目录下创建index.html文件&#xff1a; sudo mkdir -p…

C语言格式化输入输出总结 (printf和scanf)

一、printf格式化输出 1. 整数格式化 (%d, %i, %u, %o, %x) c复制代码 int num 42; // 以下为不同格式输出示例 printf("%d", num); // 42 (十进制) printf("%i", num); // 42 (同%d) printf("%u", num); // 42 (无符号十进制…