【JavaEE初阶】多线程重点知识以及常考的面试题-多线程进阶(一)

本篇博客给大家带来的是多线程中常见的所策略和CAS知识点.
🐎文章专栏: JavaEE初阶
🚀若有问题 评论区见
❤ 欢迎大家点赞 评论 收藏 分享
如果你不知道分享给谁,那就分享给薯条.
你们的支持是我不断创作的动力 .
王子,公主请阅🚀
- 要开心
- 要快乐
- 顺便进步
- 1. 常见的锁策略
- 1.1 锁策略的相关面试题
- 1.1.1 介绍一下读写锁?
- 1.1.2 说明是自旋锁,为什么要使用自旋锁策略呢? 它的缺点是什么?
- 1.1.3 synchronized 是可重入锁么?
- 2. CAS
- 2.1 什么是CAS
- 2.2 CAS是怎么实现的?
- 2.3 CAS有哪些应用?
- 2.3.1 应用一CAS实现原子类
- 2.3.2 实现自旋锁
- 2.4 CAS的ABA问题
- 2.4.1 什么是ABA问题
- 2.4.2 ABA问题引来的bug
- 2.5 CAS的相关面试题
要开心
要快乐
顺便进步
1. 常见的锁策略
乐观锁与悲观锁
悲观锁:
总是以最坏的情况考虑, 每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样一来如果其他线程想拿到这个数据就会阻塞等待直到拿到锁为止.
乐观锁:
假设数据⼀般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
重量级锁和轻量级锁
锁的核心特性 “原子性”, 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
①CPU 提供了 “原子操作指令”.
②操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁
③JVM 基于操作系统提供的互斥锁, 实现了 synchronized和ReentrantLock 等关键字和类.
重量级锁:
重量级锁的加锁机制重度依赖了OS提供的mutex.
通常会进行大量的内核用户态切换,很容易引发线程的调度,成本较高.
轻量级锁:
轻量级锁的加锁机制尽可能不使用mutex, 而是尽量在用户态代码完成. 实在不行, 再使用 mutex.
少量的内核态用户态切换,不太容易引发线程调度.
自旋锁和挂起等待锁
自旋锁:
如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.一旦锁被其他线程释放, 就能第一时间获取到锁.
优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源.
挂起等待锁:
如果获取锁失败,不会立即尝试获取锁, 被动等待锁没人用的时候,才能拿到.
优点: 不消耗CPU资源.
缺点: 不能第一时间拿到锁.
公平锁和非公平锁
公平锁:
遵守"先来后到"原则, 当B与C共同竞争同一把锁, B比C先来,B就能就能先于C获取到锁.
非公平锁:
不遵守"先来后到",B和C都有可能获取到锁.
Ⅰ操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果想实现公平锁, 就需要依赖额外的数据结构(队列), 来记录线程的先后顺序.
Ⅱ 公平锁和非公平锁没有好坏之分, 关键还是看适用场景.
可重入锁和不可重入锁
可重入锁:
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。而 Linux 系统提供的 mutex 是不可重入锁.
不可重入锁:
一个线程没有释放锁,然后又尝试再次加锁.第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程没有释放第一个锁, 这时候就会死锁.
读写锁
读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.
ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
• ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock方法进行加锁解锁.
其中,
①读加锁和读加锁之间, 不互斥.
②写加锁和写加锁之间, 互斥.
③读加锁和写加锁之间, 互斥.
注意, 只要是涉及到 “互斥”, 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多久了.因此尽可能减少 “互斥” 的机会, 就是提高效率的重要途径.
synchronized属于哪一种锁?
对于"悲观乐观",是自适应的.
对于"重量轻量",是自适应的.
对于"自旋或者挂起等待",也是自适应的.
synchronized不是读写锁,是可重入锁,也是非公平锁.初始情况下,synchronized会预测当前的锁冲突概率,如果冲突概率不大,此时以乐观锁的模式来运行.(也就是轻量级锁,基于自旋锁的方式实现).
在实际使用过程中,如果发现锁冲突的情况较多,synchronized就会升级成悲观锁(也就是重量级锁,基于挂起等待的方式实现).
1.1 锁策略的相关面试题
1.1.1 介绍一下读写锁?
读写锁就是把读操作和写操作分别进行加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要用在 “频繁读, 不频繁写” 的场景中.
1.1.2 说明是自旋锁,为什么要使用自旋锁策略呢? 它的缺点是什么?
如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试
会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.
相比于挂起等待锁,
优点: 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景
下非常有用.
缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源.
1.1.3 synchronized 是可重入锁么?
是可重入锁.
可重入锁指的就是连续两次加锁不会导致死锁.
实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程则计数器自增.
2. CAS
2.1 什么是CAS
CAS: 全称Compare and swap,字面意思:”比较并交换“,
假设内存中的原数据V,旧的预期值A,需要修改的新值B。CAS(V,A,B); 涉及到以下操作:
① 比较A与V是否相等.(比较)
② 如果比较相等,将B写入V.(交换)
③ 返回操作是否成功.(返回true或者false)
CAS 可以视为是⼀种乐观锁. (或者可以理解成 CAS 是乐观锁的⼀种实现方式)
2.2 CAS是怎么实现的?
针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
① java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
② unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
③ Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。
简而言之,是因为有了硬件的支持,软件层面才能实现CAS。
2.3 CAS有哪些应用?
2.3.1 应用一CAS实现原子类
Java标准库中提供
java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.

典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.
我们来看一下getAndIncrement 的实现过程.
伪代码实现:
class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {oldValue = value;}return oldValue;}

假设两个线程同时调用 getAndIncrement
① 两个线程都读取 value 的值到 oldValue 中. (oldValue 是⼀个局部变量, 在栈上. 每个线程有自己的栈)
② 线程1 先执行 CAS 操作. 由于 oldValue 和 value 的值相同, 直接进行对 value 赋值.

③ 线程2 执行 CAS 操作, 第一次 CAS 的时候发现 oldValue 和 value 不相等, 不能进行赋值. 因此需要进入循环.在循环里重新读取 value 的值赋给 oldValue

④ 接下来线程2 第二次执行 CAS 操作, 此时 oldValue 和 value 相同, 于是直接执行赋值操作

⑤ 线程1 和 线程2 返回各自的 oldValue 的值即可
public class Demo30 {public static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {//count++;count.getAndIncrement();// ++count;//count.incrementAndGet();//count--//count.getAndDecrement();//--count//count.decrementAndGet();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {//count++;count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count.get());//10000}
}
总结: 前面说的"线程不安全" 本质上是进行自增的过程中穿插执行了. CAS让这里的自增不会穿插执行.核心思路和加锁类似.加锁是通过阻塞的方式避免穿插执行.CAS则是会通过重试的方式避免穿插.
2.3.2 实现自旋锁
自旋锁伪代码:
public class SpinLock {private Thread owner = null;public void lock(){// 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就⾃旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner = null;}
}
CAS也是多线程编程的一种重要技巧,虽然实际开发中直接使用CAS的概率不大.但是经常会用到一些内部封装了CAS的操作.
2.4 CAS的ABA问题
2.4.1 什么是ABA问题
假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.
接下来, 线程 t1 想使用 CAS 把 num 值改成 B,
那么就需要①先读取 num 的值, 记录到 oldNum 变量中.
②使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 B.
但是, 在 t1 执行这两个操作之前, t2 线程可能把 num 的值从 A 改成了B, 又从 B 改成了 A.
线程t1的CAS默认num不变就修改.但是num的值已经被 t2 给改了.只不过又改回A了. 这个时候 t1 是否要更新num的值为 B呢?
关键的问题是, t1 线程无法区分当前这个变量始终是 A, 还是经历了一个变化过程.
2.4.2 ABA问题引来的bug
大部分情况下,t2 线程这样的一个反复横跳的改动,对于t1 是否修改num是没有影响的. 但是不排除一些特殊情况.
假设 小丑老哥 有 100 存款. 小丑想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50 操作.
我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.
如果使用 CAS 的方式来完成这个扣款过程就可能出现问题.
正常的过程
① 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
② 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
③ 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.
异常的过程
① 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
② 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
③ 在线程2 执行之前, 小丑的朋友滑稽正好给小丑转账 50, 账户余额变成 100 !!
④ 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作.
这个时候, 扣款操作被执行了两次!!! 都是 ABA 问题导致的!!
解决方案:
给要修改的值,引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
CAS 操作在读取旧值的同时, 也要读取版本号.
真正修改的时候,
① 如果当前版本号和读到的版本号相同,则修改数据,并把版本号+1.
② 如果当前版本号高于读到的版本号.就操作失败(认为数据已经被修改过了)
2.5 CAS的相关面试题
1. 讲解一下自己理解的CAS机制
全称 Compare and swap, 即 “比较并交换”. 相当于通过⼀个原子的操作, 同时完成 “读取内存, 比较是
否相等, 修改内存” 这三个步骤. 本质上需要 CPU 指令的支撑.
2. ABA问题怎么解决?
给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期. 如
果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当前版
本号比之前读到的版本号大, 就认为操作失败.
本篇博客到这里就结束啦, 感谢观看 ❤❤❤
🐎期待与你的下一次相遇😊😊😊
相关文章:
【JavaEE初阶】多线程重点知识以及常考的面试题-多线程进阶(一)
本篇博客给大家带来的是多线程中常见的所策略和CAS知识点. 🐎文章专栏: JavaEE初阶 🚀若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子,公主请阅🚀 要开心要快…...
ECA 注意力机制:让你的卷积神经网络更上一层楼
ECA 注意力机制:让你的卷积神经网络更上一层楼 在深度学习领域,注意力机制已经成为提升模型性能的重要手段。从自注意力(Self-Attention)到各种变体,研究人员不断探索更高效、更有效的注意方法。今天我们要介绍一种轻…...
基于定时器查询模式的LED闪烁(STC89C52单片机)
#include <reg52.h> sbit LED P0^0; sbit ADDR0 P1^0; sbit ADDR1 P1^1; sbit ADDR2 P1^2; sbit ADDR3 P1^3; sbit ENLED P1^4; void main() { unsigned char cnt 0; //定义一个计数变量,记录T0溢出次数 ENLED 0; //使能U3,选择…...
SAP系统生产跟踪报表入库数异常
生产跟踪报表入库数异常 交库21820,入库43588是不可能的 原因排查: 报表的入库数取值,是取移动类型321 (即系检验合格后过账到非限制使用)的数. 查凭证,101过账2次21807,321过账了2次21794,然后用102退1次21794.就是说这批物料重复交库了. 解决: 方案一:开发增强设…...
Kubernetes控制平面组件:调度器Scheduler(一)
云原生学习路线导航页(持续更新中) kubernetes学习系列快捷链接 Kubernetes架构原则和对象设计(一)Kubernetes架构原则和对象设计(二)Kubernetes架构原则和对象设计(三)Kubernetes控…...
08-DevOps-向Harbor上传自定义镜像
harbor创建完成,往harbor镜像仓库中上传自定义的镜像,包括新建项目、docker配置镜像地址、镜像重命名、登录harbor、推送镜像这几个步骤,具体操作如下: harbor中新建项目 访问级别公开,代表任何人都可以拉取仓库中的镜…...
Vue v-for 循环DOM 指定dom个数展示一行
在Vue.js中,如果想根据v-for循环的结果来控制哪些元素应该在一行中展示,你可以通过计算属性或者方法来实现。这里使用CSS改变样式和js脚本两种方式做到这一点,根据你的具体需求选择适合的方法。 方法1:使用计算属性 如果你想要基…...
mysql控制单表数据存储及单实例表创建
1. 单表数据存储不要过大 主流建议 保守建议。100万以内保持最佳性能其他。不超过2000万 理论依据。 B树层级可能变多。从3增加到4。导致索引查询路径边长,增加IO开销 优化 加索引。对高频查询字段增加索引。避免全表扫描低频历史数据通过分区表或归档隔离。足够的…...
极验4滑块笔记:整理思路--填坑各种问题
最近在研究某验4逆向分析,以前没弄过这种,所以爬了很多坑,就是把分享给大家~ 1.这个gcaptcha4.js需要逆向,我的方法很笨就是将_ᕶᕴᕹᕶ()这个蝌蚪文打印处来,全局替换一下,然后Unicode这种代码࿰…...
LX3-初识是单片机
初识单片机 一 什么是单片机 单片机:单片微型计算机单片机的组成:CPU,RAM(内存),flash(硬盘),总线,时钟,外设…… 二 Coretex-M系列介绍 了解ARM公司与ST公司ARM内核系列: A 高性能应用,如手机,电脑…R 实时性强,如汽车电子,军工…M 超低功耗,如消费电子,家电,医疗器械 三…...
2025年渗透测试面试题总结-拷打题库10(题目+回答)
网络安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 2025年渗透测试面试题总结-拷打题库10 1. CSRF成因及防御措施 | 非Token防御 2. XSS Worm原理 3. Co…...
Linux系统下docker 安装 MySQL
踩坑解决: 1、docker安装mysql,不需要执行search 2、pull时,需要指定版本号 3、连接Navicat需要看阿里云端口号是否开启 在拉取镜像的时候,如果不使用代理服务器,docker search mysql不需要执行 本人在未使用代理服…...
配置 VS Code 使用 ESLint 格式化
1、在设置里面搜索Default Formatter,下拉框里选择eslint 2、并勾选Enables ESlint as a formatter 3、再在settings.json文件中添加配置代码,如下所示: 1) 、打开 VS Code 设置 快捷键:Ctrl ,(Mac: ⌘ ,…...
从代码实现理解Vision Permutator:WeightedPermuteMLP模型解析
从代码实现理解Vision Permutator:WeightedPermuteMLP模型解析 随着人工智能的快速发展,视觉识别任务变得越来越重要。最近提出的Vision Permutator架构为这一领域带来了新的思路,它通过可学习的排列操作重新定义了特征交互的方式。 今天我…...
Web开发:ABP框架10——使用数据库存储文件,完成文件的下载和上传
一、简要介绍 字节数组:字节数组是存储数据的字节序列,常用于二进制数据(如图片、音视频、文档等)的表示。 文件和字节的关系:文件是由字节构成,字节是文件内容的基本单位。 文件以字节形式存储在服务器数…...
SystemVerilog语法之内建数据类型
简介:SystemVerilog引进了一些新的数据类型,具有以下的优点:(1)双状态数据类型,更好的性能,更低的内存消耗;(2)队列、动态和关联数组,减少内存消耗…...
NestJS-Knife4j
文章目录 前言✅ 一、什么是 Knife4j?✅ 二、Knife4j 与 Swagger 对比✅ 三、NestJS-Knife4j 集成1. 安装依赖2. 配置 Swagger 与 Knife4j3. 启动应用并访问接口文档 ✅ 四、功能增强1. **接口分组**2. **请求/响应示例**3. **接口文档的美化** ✅ 五、总结 前言 N…...
【项目管理】成本类计算 笔记
项目管理-相关文档,希望互相学习,共同进步 风123456789~-CSDN博客 (一)知识总览 项目管理知识域 知识点: (项目管理概论、立项管理、十大知识域、配置与变更管理、绩效域) 对应&…...
手机投屏到电视方法
一、投屏软件 比如乐播投屏 二、视频软件 腾讯视频、爱奇艺 三、手机无线投屏功能 四、有线投屏 五、投屏器...
(三十)安卓开发中的MVP模式详解
在安卓开发中,MVP(Model-View-Presenter) 是一种常见的软件架构模式,它通过将应用程序的逻辑与用户界面分离,使得代码更加模块化、易于维护和测试。本文将详细讲解MVP模式的组成部分、工作流程、优点,并结合…...
基于MuJoCo物理引擎的机器人学习仿真框架robosuite
Robosuite 基于 MuJoCo 物理引擎,能支持多种机器人模型,提供丰富多样的任务场景,像基础的抓取、推物,精细的开门、拧瓶盖等操作。它可灵活配置多种传感器,提供本体、视觉、力 / 触觉等感知数据。因其对强化学习友好&am…...
配置管理CM
以下是关于项目管理中 配置管理 的详细解析,结合高项(如软考高级信息系统项目管理师)教材内容,从理论到实践进行系统阐述: 一、配置管理的基本概念 1. 定义 配置管理(Configuration Management, CM)是识别、记录、控制项目成果(产品、服务或过程)的物理和功能特征,…...
13.编码器的结构
从入门AI到手写Transformer-13.编码器的结构 13.编码器的结构代码 整理自视频 老袁不说话 。 13.编码器的结构 T r a n s f o r m e r E n c o d e r : 输入 [ b , n ] TransformerEncoder:输入[b,n] TransformerEncoder:输入[b,n] E m b e d d i n g : − > [ b , n , d ]…...
[原理分析]安卓15系统大升级:Doze打盹模式提速50%,续航大幅增强,省电提升率5%
技术原理:借鉴中国友商思路缩短进入Doze的时序 开发者米沙尔・拉赫曼(Mishaal Rahman)在其博文中透露,谷歌对安卓15系统进行了显著优化,使得设备进入“打盹模式”(Doze Mode)的速度提升了50%,并且部分机型的待机时间因此得以延长三小时。设备…...
cdp-(Chrome DevTools Protocol) browserscan检测原理逆向分析
https://www.browserscan.net/zh/bot-detection 首先,打开devtools后访问网址,检测结果网页显示红色Robot,标签插入位置,确定断点位置可以hook该方法,也可以使用插件等方式找到这个位置,本篇不讨论. Robot标签是通过insertBefore插入的. 再往上追栈可以发现一个32长度数组,里面…...
【Java面试笔记:基础】1.谈谈你对Java平台的理解?
前言 Java 是历史悠久且主流的编程语言,拥有庞大的开发者群体和广泛的应用领域。通过系统学习和实践,构建扎实的 Java 知识体系,提升面试成功率 笔记核心内容 1. Java 平台的核心特性 跨平台特性:Java 的核心特性之一是“Writ…...
Containerd与Docker的相爱相杀:容器运行时选型指南
容器运行时(Container Runtime)作为云原生基础设施的底层引擎,正从Docker一家独大走向多元化竞争。本文将深入剖析Containerd与Docker的技术血缘、性能差异及选型策略,揭示如何根据场景需求选择最优解。 一、技术血缘:…...
Java第五节:继承thread类创建线程
1、创建类Thread01 创建类Thread01然后继承thread类 2、重写run函数 3、运行线程 在主函数创建两个线程,并执行。...
vite安装及使用
没特殊要求的项目,还是怎么简单怎么来╮(╯▽╰)╭ 一、Vite 基础知识 1. 什么是 Vite? Vite 是一个前端构建工具,专注于开发服务器速度和优化构建过程。特点: 快速冷启动:利用 ES 模块的原生支持,实现快速的开发服务器启动。即时热更新:在开发过程中,修改代码后可以…...
Oracle expdp的 EXCLUDE 参数详解
Oracle expdp的 EXCLUDE 参数详解 EXCLUDE 是 Oracle Data Pump Export (expdp) 工具中的一个关键参数,用于指定在导出过程中要排除的对象或对象类型。 一、基本语法 expdp username/password DUMPFILEexport.dmp DIRECTORYdpump_dir EXCLUDEobject_type[:name_c…...
