首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【JavaSE】常见的锁策略 && CAS详解 && synchronized 原理

【JavaSE】常见的锁策略 && CAS详解 && synchronized 原理

原创
作者头像
lirendada
发布2026-04-12 12:02:40
发布2026-04-12 12:02:40
970
举报
文章被收录于专栏:JavaJava

Ⅰ. 常见的锁策略

一、乐观锁 && 悲观锁

  • 悲观锁:每次取数据时,总是担心数据会被其它线程修改,所以会在取数据前先加锁,当其它线程想要访问数据时就会被阻塞。
    • 典型案例:数据库行锁、synchronizedReentrantLock
    • 优点:在并发访问中保证了数据的一致性
    • 缺点:会造成较高的锁竞争,降低并发性能,因为当一个线程持有锁时,其他线程只能等待
  • 乐观锁:每次取数据时,总是乐观认为数据不会被其它线程修改,因此不会对数据进行上锁,而会采用 "忙等" 或者版本号机制来代替上锁。通常在更新数据数据前,会判断其他数据在更新前有没有对数据进行修改,如果没有则进行更新操作,否则进行相应的补偿操作,
    • 典型案例:数据库版本号、Atomic类(CAS)、时间戳机制
    • 优点:并发度高,因为不需要加锁,所以不会出现因为线程竞争导致的阻塞现象
    • 缺点:在高并发环境下,乐观锁的重试操作可能会比较频繁,导致性能下降。此外乐观锁的实现需要对数据进行版本号的维护,这需要增加额外的开销。

悲观锁

乐观锁

吞吐量

低(锁竞争导致线程阻塞和上下文切换)

高(无锁竞争,适合高并发读场景)

冲突处理

直接阻塞,避免冲突

冲突时回滚或重试(自旋)

适用场景

写操作频繁、冲突概率高(如银行转账、库存扣减)

读操作为主、冲突概率低(如商品浏览、配置读取)

死锁风险

高(需严格按顺序释放锁)

实现复杂度

低(依赖数据库或语言原生锁)

中(需设计版本号或CAS逻辑)

二、重量级锁 && 轻量级锁

锁的核心特性 "原子性",这种机制追根溯源是 CPU 这样的硬件设备提供的:

  • CPU 提供了 "原子操作指令"
  • 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁
  • JVM 基于操作系统提供的互斥锁, 实现了 synchronizedReentrantLock 等关键字和类

注意:synchronized 并不仅仅是对 mutex 进行封装,synchronized 内部还做了很多其它的工作,下面会详细介绍!

  • 重量级锁:加锁机制重度依赖 OS 提供的 mutex
    • 大量的内核态与用户态切换
    • 容易引发线程调度
  • 轻量级锁:加锁机制尽可能不使用 OS 提供的 mutex,而是尽量在用户态代码完成互斥操作,当实在搞不定的时候,才使用 mutex
    • 少量的内核态与用户态切换
    • 不太容易引发线程调度

下面在介绍 synchronized 原理的时候会介绍两者的关系!

三、自旋锁 && 挂起等待锁

  • 自旋锁:在获取不到锁的情况下,不断地尝试获取锁,直到获取成功或者超过一定次数之后再进行等待
    • 适用于锁被持有的时间比较 的情况。例如在多处理器系统上使用自旋锁,因为在这种情况下,线程等待锁的时间很短,使用自旋锁可以避免线程切换的开销,提高性能。
    • 所以自旋锁的缺点就是如果锁被持有的时间较长,那么就会持续消耗 CPU 资源!
  • 挂起等待锁:在获取不到锁的情况下,线程会进入阻塞状态,让出 CPU,等待锁的持有者释放锁之后被唤醒
    • 适用于锁被持有的时间比较 的情况。例如在单处理器系统上使用互斥锁,因为在这种情况下,线程等待锁的时间比较长,使用自旋锁会浪费大量的 CPU 资源。
    • 挂起等待锁的优点就是不会消耗 CPU 资源,因为一旦阻塞就会让出 CPU 使用权!

四、公平锁 && 非公平锁

  • 公平锁遵守 "先来后到" 规则。比如 BC 先来,当 A 释放锁的之后,B 先于 C 获取到锁。
  • 非公平锁不遵守 "先来后到" 规则。比如 BC 先来,当 A 释放锁的之后,BC 都有可能获取到锁。
  • 这两种锁没有好坏之分,关键还要看适用场景!

五、可重入锁 && 不可重入锁

可重入锁:允许同一个线程多次获取同一把锁,反之则为不可重入锁。

Java 里只要以 Reentrant 开头命名的锁都是可重入锁,而且 JDK 提供的所有现成的 Lock 实现类,包括 synchronized 锁也是可重入的。

Linux 系统提供的 mutex 是不可重入锁。

六、读写锁

读写锁允许多个线程同时读取资源,但在写入资源时需要进行互斥访问

读写锁分为 共享锁(读锁)排他锁(写锁),当一个线程获得共享锁时,其他其它线程也可以获得共享锁,但是不能获得排他锁;当一个线程获得排他锁时,其它线程无法获得共享锁和排他锁。

Java 标准库提供了 ReentrantReadWriteLock 类,实现了读写锁:

  • ReentrantReadWriteLock.ReadLock 类表示读锁,这个对象提供了 lock/unlock 方法进行加锁解锁
  • ReentrantReadWriteLock.WriteLock 类表示写锁,这个对象也提供了 lock/unlock 方法进行加锁解锁

Ⅱ. CAS

一、什么是 CAS❓❓❓

CASCompare-And-Swap,比较并交换) 是一种无锁原子操作技术(乐观锁),用于在多线程环境下实现变量的线程安全修改。它通过 CPU 底层指令保证操作的原子性,避免了传统锁(如synchronized)带来的线程阻塞和上下文切换开销。

其伪代码如下所示:

注意:下面的伪代码并不是原子的,真实的 CAS 是一个原子的硬件指令完成的,而下面的伪代码只是辅助理解 CAS 的工作流程!

代码语言:javascript
复制
boolean CAS(address, expectValue, newValue) {
    if(&address == expectValue) {
        &address = newValue; // 注意我们只关心address是否被修改,所以这里是赋值操作,真实场景应该是交换
        return true;
    }
    return false;
}

上面 CAS 函数中通常有三个参数:共享变量的内存地址期待值要修改的值。其操作如下所示:

  1. 首先比较共享变量的值是否等于期待值(这个期待值往往是 address 所指内存原来的值)
  2. 如果相等,则将共享变量的值替换为新值,然后返回 true,表示操作成功!
  3. 如果不相等,说明其它线程可能改变过这个期待值,则不执行任何操作(怕影响到其它线程),然后返回 false,这样子操作失败的线程就能获得一个失败的信号!

在这个过程中,CAS操作具有原子性,即其他线程无法同时访问共享变量,从而避免了竞态条件和数据竞争等问题。

二、CAS 的应用

JavaCAS 的底层实现依赖于 sun.misc.Unsafe 类(JDK 内部使用,不建议直接调用)。

Unsafe 提供了一系列 compareAndSwapXxx 方法(如 compareAndSwapIntcompareAndSwapObject),这些方法直接调用 CPU 的原子指令(如 x86cmpxchg 指令),确保操作的原子性。

① 实现原子类

标准库中提供了 java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的。典型的就是 AtomicInteger 类,其中的 getAndIncrement 相当于 i++ 操作,如下所示:

代码语言:javascript
复制
AtomicInteger count = new AtomicInteger(0);
System.out.println(count.getAndIncrement()); // 相当于i++

该函数的伪代码如下所示:

代码语言:javascript
复制
class AtomicInteger {
    private volatile int value; // 实际上存放在内存中,用volatile保持内存可见性
    
    public int getAndIncrement() {
        int oldValue = value; // oldValue实际上存放在寄存器中
        
        // 如果value==oldValue,则说明没有线程改过,则赋值为OldValue+1,跳出循环,返回原先没有++的值
        // 如果value!=oldValue,则不进行赋值,收到false后进入循环更新oldValue,重新判断,直到数值相同
        while(CAS(value, oldValue, oldValue + 1) == false) { 
            oldValue = value;
        }
        return oldValue;
    }
}

💥注意事项:

  • CAS 是直接读写内存的,即 value 一直存放在内存,而读写内存对于 CAS 来说是一条硬件指令,是原子的,这在上面代码中没有体现出来,但是要知道!

② 实现自旋锁

伪代码如下所示:

代码语言:javascript
复制
public class SpinLock { 
    private Thread owner = null; 
    
    public void lock(){ 
        // 通过 CAS 判断当前锁是否被某个线程持有  
        // 如果这个锁已经被别的线程持有, 即this.owner!=null,那么就自旋等待  
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程  
        while(CAS(this.owner, null, Thread.currentThread()) == false){ 
        } 
    } 
    
    public void unlock (){ 
        this.owner = null; 
    } 
}

三、CAS 存在的 ABA 问题

CAS 操作虽然可以保证对共享变量的原子性操作,但是由于 CAS 操作涉及到对共享变量的读取和修改,因此可能存在 ABA 问题,即在多线程并发访问共享变量的情况下,共享变量的值在某个时刻被其它线程先改为 A,然后改为 B,最后又改为 A,从而导致一些问题

比如列举一个非常非常极端的例子,假设银行就是用 CAS 操作保证存钱、转账等操作是原子的,此时我去银行取 500 块钱,点击取钱按钮后,ATM 机会后台生成一个线程 A 去执行取钱操作,但是一不小心机器卡了,此时我就多点了一下取钱按钮,导致了后台可能创建了一个新线程 B,也去执行取钱操作,但我浑然不知,假设 BA 提前完成了扣款操作。与此同时朋友给我银行转账了 500 块钱,执行的是线程 C,导致 balance 变成了 1000,在极端情况下,A 最后才完成了 CAS 操作,判断相同,又扣了 500 块钱,相当于朋友的钱被银行吃了!

如何解决这个问题❓❓❓

上述问题主要是由既有加操作,又有减操作引起的,只要我们让程序只能进行加操作,就能避免这种情况!

因此引入了版本号机制约定每次修改操作,都需要对版本号加一,即在进行 CAS 操作时,不仅要比较共享变量的值是否等于期望值,还需要比较版本号是否匹配。这样可以确保即使共享变量的值在中间被修改过,但由于版本号不匹配,CAS 操作依然会失败,从而避免了上述问题的出现。

Ⅲ. synchronized 原理

一、基本特点

结合上面的锁策略,可以总结出 synchronized 具有以下特性(只考虑 JDK 1.8)

在底层实现上,synchronized 是通过 monitorentermonitorexit 指令(字节码指令)来实现锁的获取和释放。

二、锁升级策略

JVM 中,对象的锁状态可以在运行时在 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 之间逐级升级(不可降级)。

锁状态

特点

适用场景

无锁

默认状态,无同步需求

没有使用 synchronized

偏向锁

只被一个线程使用过的锁

无线程竞争

轻量级锁

多线程并发,但竞争很小

多个线程交替执行同步块,对锁竞争小

重量级锁

多线程并发,且竞争严重

多个线程同时进入同步块,阻塞

① 偏向锁

偏向锁不是真正的加锁,只是在对象头中标记锁信息为 "偏向锁",只有当其它线程也来竞争该锁的时候,才会取消偏向锁状态,转变为 "轻量级锁"。这样子做的好处有点像 "懒汉模式" 的延迟启动,尽量避免不必要的开锁开销,因为不一定真的有线程来竞争锁!

对象头结构JVM 用来管理对象运行时信息的元数据容器,每个 Java 对象在内存中分为三部分

  • 对象头Header):存储锁状态、GC 分代年龄、哈希码等
  • 实例数据Instance Data):对象的字段值
  • 对齐填充Padding):补齐内存对齐

② 轻量级锁

偏向锁被第二个线程访问时会撤销并升级为轻量级锁,轻量级锁通过 CAS 以及自旋来代替线程阻塞,减少用户态与内核态切换的成本,步骤可以参考上面介绍 CAS 中实现自旋锁的那段伪代码,逻辑差不多!

③ 重量级锁

如果竞争进一步激烈,自旋不能快速获取到锁状态,synchronized 就会自动转变为重量级锁,即采用内核提供的 mutex 来保证线程安全!步骤大概如下所示:

  1. 执行 mutex 加锁操作,进入内核态
  2. 在内核态判定当前锁是否已经被占用
  3. 如果该锁没有占用,则加锁成功,并切换回用户态
  4. 如果该锁被占用,则加锁失败,此时线程被挂起,进入锁的等待队列,等待操作系统唤醒
  5. 经历了一系列的沧海桑田,这个锁被其它线程释放了,操作系统也想起了这个挂起的线程,于是唤醒这个线程,尝试重新获取锁!

三、锁消除

锁消除是 JVM 的一种优化技术,当编译器和 JVM 检测到同步代码块实际上并不存在共享数据的竞争,则会直接移除对应的同步指令,提高效率

举个例子,本身就是线程安全的 StringBuffer,可能被一些程序员用 synchronized 加锁保护起来,但压根没必要加锁,所以这些代码实际上都会被移除!

四、锁粗化

锁粗化是 JVM 针对高频次加锁场景的优化技术,通过合并相邻的同步块来减少锁竞争和上下文切换的开销。当编译器检测到多个连续的同步操作使用同一锁对象时(例如循环体内反复加锁或相邻代码段重复获取同一锁),会将多个细粒度的锁区域合并为一个更大的锁范围,从而将原本多次的加锁/解锁操作简化为单次操作。

举个很简单的例子,你有三个任务要向老板汇报,你分三次打电话过去汇报每个任务,和分一次去汇报三个任务的结果是一样的,但这个过程中显然后者更省力更高效!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Ⅰ. 常见的锁策略
    • 一、乐观锁 && 悲观锁
    • 二、重量级锁 && 轻量级锁
    • 三、自旋锁 && 挂起等待锁
    • 四、公平锁 && 非公平锁
    • 五、可重入锁 && 不可重入锁
    • 六、读写锁
  • Ⅱ. CAS
    • 一、什么是 CAS❓❓❓
    • 二、CAS 的应用
      • ① 实现原子类
      • ② 实现自旋锁
    • 三、CAS 存在的 ABA 问题
  • Ⅲ. synchronized 原理
    • 一、基本特点
    • 二、锁升级策略
      • ① 偏向锁
      • ② 轻量级锁
      • ③ 重量级锁
    • 三、锁消除
    • 四、锁粗化
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档