news 2026/10/20 5:00:19

[JavaEE]多线程进阶(一)------(含面试题)常见的锁策略,CAS,synchronized原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
[JavaEE]多线程进阶(一)------(含面试题)常见的锁策略,CAS,synchronized原理

[JavaEE]多线程进阶(一)------常见的锁策略,CAS,synchronized原理

摘要:本文是 JavaEE 多线程进阶系列的第二篇,主要讲解了常见的锁策略,CAS,synchronized原理.每个部分都给出了对应的面试题.

目录

1.常见的锁策略

相关面试题

2.CAS

相关面试题

3.synchronized原理

相关面试题

正文

1.常见的锁策略

如果某一天,我们认为标准库中提供的锁不够用,这个时候我们就要关注锁策略.(实际开发中synchronized已经非常好用了,足以覆盖绝大多数的使用场景).此处的锁策略不仅仅是局限于Java这门语言,其他语言,但凡涉及到并发编程,涉及到锁都可以谈到这样的锁策略(这个锁在加锁的时候有什么特点,有什么行为).

1.1悲观锁&乐观锁

悲观锁和乐观锁不是针对某一种具体情况的锁,而是某个具体的锁具有"悲观"或者"乐观"这样的特性.是两种并发控制策略.

1.2重量级锁&轻量级锁

悲观锁和乐观锁描述的是,加锁时候遇到的场景.重量级锁和轻量级锁则是描述遇到场景之后的解决方案.

重量级锁和轻量级锁的区别,本质上在于锁的实现是否依赖操作系统内核,这就引出了内核态与用户态的概念:

重量级锁依赖操作系统提供的互斥量(Mutex)来实现。加锁、解锁时,线程需要从用户态切换到内核态,。这种切换开销很大,效率较低,适合锁竞争激烈的时候.
轻量级锁(如synchronized的轻量级锁、ReentrantLock的 CAS 实现)则不依赖内核,直接在用户态通过 CAS(Compare And Swap)等原子操作完成加锁。整个过程不涉及内核态切换,开销小、速度快,适合竞争不激烈的场景.

1.3挂起锁&自旋锁

1.4普通互斥锁&读写锁

读写锁的核心思想是:读读共享、读写互斥、写写互斥。它适合读多写少的场景。(如服务器开发中的常见场景,MySQL索引,查询等)
注意:只要涉及“互斥”,就会产生线程的挂起等待。一旦挂起等待,再次被唤醒就不知道要等多久了。因此尽可能减少“互斥”的可能。因此读写锁适合读多写少的场景。(如服务器开发中的常见场景,MySQL索引,查询等)
读写锁就是把读操作和写操作区分对待。Java标准库中提供了ReentrantReadWriteLock类实现了读写锁。

1.5可重入锁&不可重入锁

1.6公平锁&非公平锁

假设有三个线程A,B,C竞争锁。A拿到了锁,然后这个时候B先尝试获取锁,获取失败,于是阻塞等待。接着C尝试获取锁,获取失败,于是阻塞等待。当A释放锁的时候,会发生什么呢?

相关面试题

1.你是怎么理解悲观锁和乐观锁的?具体怎么实现?

2.介绍下读写锁?

3.什么是自旋锁?为什么要使用自旋锁策略?缺点是什么?

4.synchronized是可重入锁吗?

2.CAS

2.1什么是CAS?

CAS:全称Compare and swap,字面意思:“比较并交换”,先给出CAS的伪代码:(伪代码不能真正运行,是表达我们代码逻辑的一种手段)

booleanCAS(address,expectValue,swapValue){if(address==expectValue){address==swapValue)returntrue;}returnfalse;}

一个CAS涉及到以下操作:

2.2CAS有哪些应用?

1.实现原子类
jav标准库中提供了java.util.concurrent.atomic类,里面的类都是基于这种方式实现的.
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/atomic/package-summary.html

典型的就是AtomicInteger类.其中getAndIncrement相当于i++操作.

importjava.util.concurrent.atomic.AtomicInteger;publicclassDemo01{publicstaticvoidmain(String[]args){//初始值0AtomicIntegeratomicInteger=newAtomicInteger(0);//相当于i++,保证了线程安全,但却不用加锁.atomicInteger.incrementAndGet();System.out.println(atomicInteger.get());//输出1}}

上述代码,使用原子类AtomicInteger完成了++操作.count++操作是线程不安全的,但是又认为加锁效率比较低,于是可以通过CAS来实现count++,确保新能又保证线程安全.
2.实现自旋锁
自旋锁的伪代码

publicclassSpinLock{privateThreadowner=null;//记录锁属于哪个线程publicvoidlock(){//通过CAS看当前锁是否被某个线程持有//如果这个线程已经被其他线程所持有,那么就进入忙等状态,即自旋等待//如果没有被其他线程所持有,那么就把owner设置为当前尝试加锁的线程while(!CAS(this.owner,null,Thread.currentThread())){//如果锁持有者==null,即this.owner==null,那么进行交换操作就把锁持有者owner==Thread.currentThread.// return True,取反为False,此时退出循环//反之,若锁的持有者不为空,即此时被其他线程占用,this.owner!=null,那么不会进行交换操作,// return False,取反则为True,进入死循环,即忙等自旋状态.}}publicvoidunlock(){this.owner=null;//释放锁了就把线程持有者置为空}}

2.3CAS的ABA问题


使用CAS能够进行线程安全的编程,核心就是先比较’‘相等’',比较address和expectValue是否相等.比较address和expectValue是否相等的本质就是在判断是否有其他线程穿插进来,做了一些修改.认为如果这里的address和expectValue一致,则认为没有线程穿插进来修改,因此认为接下来的修改操作是线程安全的.但是实际上存在这样的一种情况:另一个线程把address改成了另外的值,又从这个值改回成原来的address的值.此时有其他线程穿插进来修改,但是值不变,就会依旧认为这个线程是安全的.
举例:转账

我的余额有1000,取500,我点击"取款",没反应,于是我连续点击了两次.

解决方案:引入版本号
给要修改的值额外关联一个版本号,每次修改数据时版本号 +1。这样 CAS 比较的就不再是「数据值本身」,而是「版本号」:

为什么能解决 ABA 问题?

回到转账的例子:即使余额从 1000 → 500 → 1000(值没变),但版本号已经 1 → 2 → 3,版本号不会变回原来的值。第二次点击取款时,读到的版本号是 3,而当前版本号也是 3,但中间其实已经发生过两次修改——不过由于版本号单调递增,只要版本号变了,就能识别出「数据被动过」,从而拒绝操作。

相关面试题

1.讲解下你所理解的CAS机制?

2.ABA问题怎么解决?
给要修改的数据引入版本号.在CAS比较数据当前值和旧值得同时,也要比较版本号是否符合预期.如果发现当前版本号和之前读到得版本号一致,就真正执行修改操作,并让版本号自增;如果发现当前数据得版本号比之前读到得版本号大,就认为操作失败.

3.synchronized原理

3.1基本特点

结合上面的锁策略,synchronized有以下特性:

3.2synchronized的锁升级过程

synchronized的加锁过程是一个自适应的过程,会根据实际情况进行升级.会按照以下的一个升级顺序:

下面解释一下synchronized的锁升级详细过程:
(1)无锁:
(2)偏向锁(并不是真的加速,知识做一个标记):
第一个尝试加锁的线程,优先进入偏向锁状态.

3.3其他优化作用

1.锁消除(锁直接消失)
锁消除也是编译器优化的一种.编译器会判定当前这个代码逻辑是否需要真正的加锁,如果不需要加锁,但是又写了synchronized,就会自动把锁去掉.

锁消除的代码示例:
下面这段代码中,sb是方法内部的局部变量,不会被其他线程共享。编译器能分析出这一点,于是判定这里的synchronized加锁没有实际意义,会自动把锁去掉(即锁消除):

publicclassDemo03{publicstaticvoidmain(String[]args){// sb 是局部变量,不会被其他线程共享StringBuildersb=newStringBuilder();for(inti=0;i<1000;i++){// StringBuilder 的 append 方法内部有 synchronized 修饰// 但由于 sb 不会被其他线程访问,编译器会消除这里的锁sb.append(i);}System.out.println(sb.length());}}

2.锁粗化(减少锁的数量)
锁粗化也是编译器优化的一种.一段代码中出现多次加锁,编译器+JVM会自动进行锁的粗化.
此时要引入另一个概念:锁的粒度.
锁的粒度分为粗和细.
在正常情况下我们要缩小锁的粒度以提高并发;但如果缩得太小导致频繁开关锁太慢,JVM 就会自动把锁的粒度适当放大(粗化)来提高效率。
**举例:**你给领导汇报工作(包括了三项工作内容)
方式一:

相关面试题

1.什么是偏向锁?

2.synchronized的实现原理是什么?
参考上述章节.

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 5:00:02

Skills 底层逻辑与实战:从技能拆解到组合应用

1. 从“skills”这个词说起&#xff1a;为什么它突然成了硬通货最近后台收到不少私信&#xff0c;问的都是同一类问题&#xff1a;“现在到处都在聊 skills&#xff0c;它到底指什么&#xff1f;跟我有什么关系&#xff1f;”说实话&#xff0c;第一次看到这个词被单独拎出来当…

作者头像 李华
网站建设 2026/10/20 4:58:31

第 10 篇 Scatter-Gather 与 sg_table:分散内存的统一映射

本文属于第三册“内核抽象&#xff1a;dma-mapping 接口的设计原则”&#xff0c;回答第 0 篇提出的问题 10&#xff1a; 物理上分散的内存为何能以连续地址的形式提供给设备&#xff1f;dma_map_sg 完成了哪些工作&#xff1f; 本文讨论内核如何在映射接口层面统一处理设备的…

作者头像 李华
网站建设 2026/10/20 4:57:06

桶排序详解:从分桶思想到 O(n) 时间复杂度的工程实现

基础算法集训第09天&#xff0c;我们把排序从“比较”这条路上拐了个弯&#xff0c;来聊桶排序&#xff08;Bucket Sort&#xff09;。这次集训前面几天基本都在过常规排序&#xff1a;插入、选择、快排、归并……这些算法再花哨&#xff0c;本质都是通过两两比较去确定元素顺序…

作者头像 李华
网站建设 2026/10/20 4:56:21

Java WebSocket断线重连:心跳、退避重连与消息补偿实战

做实时推送、在线IM、行情订阅这类系统&#xff0c;只要网络一波动&#xff0c;WebSocket连接就可能悄悄断掉&#xff0c;而你的代码如果不做处理&#xff0c;用户那边就永远卡在一个"连接已断开"的假死状态。这算是Java后端做WebSocket最常踩的坑之一&#xff1a;客…

作者头像 李华
网站建设 2026/10/20 4:55:46

Antigravity+ChatCut组合,打造AI视频剪辑速度流工作流

从去年开始&#xff0c;我越来越频繁地被同行问同一个问题&#xff1a;为什么你出片能这么快&#xff1f;同样一条口播视频&#xff0c;别人还在拉时间轴、找素材、抽字幕的时候&#xff0c;我已经把成片导出在群文件里了。说实话&#xff0c;不是我的剪辑手速变快了&#xff0…

作者头像 李华