
synchronized、ReentrantLockAtomic类(CAS)、时间戳机制悲观锁 | 乐观锁 | |
|---|---|---|
吞吐量 | 低(锁竞争导致线程阻塞和上下文切换) | 高(无锁竞争,适合高并发读场景) |
冲突处理 | 直接阻塞,避免冲突 | 冲突时回滚或重试(自旋) |
适用场景 | 写操作频繁、冲突概率高(如银行转账、库存扣减) | 读操作为主、冲突概率低(如商品浏览、配置读取) |
死锁风险 | 高(需严格按顺序释放锁) | 无 |
实现复杂度 | 低(依赖数据库或语言原生锁) | 中(需设计版本号或CAS逻辑) |
锁的核心特性 "原子性",这种机制追根溯源是 CPU 这样的硬件设备提供的:
CPU 提供了 "原子操作指令"CPU 的原子指令, 实现了 mutex 互斥锁JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类
注意:synchronized 并不仅仅是对 mutex 进行封装,在 synchronized 内部还做了很多其它的工作,下面会详细介绍!
OS 提供的 mutexOS 提供的 mutex,而是尽量在用户态代码完成互斥操作,当实在搞不定的时候,才使用 mutex。下面在介绍 synchronized 原理的时候会介绍两者的关系!
CPU 资源!CPU,等待锁的持有者释放锁之后被唤醒。CPU 资源。CPU 资源,因为一旦阻塞就会让出 CPU 使用权!B 比 C 先来,当 A 释放锁的之后,B 先于 C 获取到锁。B 比 C 先来,当 A 释放锁的之后,B 和 C 都有可能获取到锁。可重入锁:允许同一个线程多次获取同一把锁,反之则为不可重入锁。
Java 里只要以 Reentrant 开头命名的锁都是可重入锁,而且 JDK 提供的所有现成的 Lock 实现类,包括 synchronized 锁也是可重入的。
而 Linux 系统提供的 mutex 是不可重入锁。
读写锁允许多个线程同时读取资源,但在写入资源时需要进行互斥访问。
读写锁分为 共享锁(读锁) 和 排他锁(写锁),当一个线程获得共享锁时,其他其它线程也可以获得共享锁,但是不能获得排他锁;当一个线程获得排他锁时,其它线程无法获得共享锁和排他锁。
Java 标准库提供了 ReentrantReadWriteLock 类,实现了读写锁:
ReentrantReadWriteLock.ReadLock 类表示读锁,这个对象提供了 lock/unlock 方法进行加锁解锁ReentrantReadWriteLock.WriteLock 类表示写锁,这个对象也提供了 lock/unlock 方法进行加锁解锁CAS❓❓❓CAS(Compare-And-Swap,比较并交换) 是一种无锁的原子操作技术(乐观锁),用于在多线程环境下实现变量的线程安全修改。它通过 CPU 底层指令保证操作的原子性,避免了传统锁(如synchronized)带来的线程阻塞和上下文切换开销。
其伪代码如下所示:
注意:下面的伪代码并不是原子的,真实的
CAS是一个原子的硬件指令完成的,而下面的伪代码只是辅助理解CAS的工作流程!
boolean CAS(address, expectValue, newValue) {
if(&address == expectValue) {
&address = newValue; // 注意我们只关心address是否被修改,所以这里是赋值操作,真实场景应该是交换
return true;
}
return false;
}上面 CAS 函数中通常有三个参数:共享变量的内存地址、期待值、要修改的值。其操作如下所示:
address 所指内存原来的值)true,表示操作成功!false,这样子操作失败的线程就能获得一个失败的信号!在这个过程中,CAS操作具有原子性,即其他线程无法同时访问共享变量,从而避免了竞态条件和数据竞争等问题。
CAS 的应用Java 中 CAS 的底层实现依赖于 sun.misc.Unsafe 类(JDK 内部使用,不建议直接调用)。
Unsafe 提供了一系列 compareAndSwapXxx 方法(如 compareAndSwapInt、compareAndSwapObject),这些方法直接调用 CPU 的原子指令(如 x86 的 cmpxchg 指令),确保操作的原子性。
标准库中提供了 java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的。典型的就是 AtomicInteger 类,其中的 getAndIncrement 相当于 i++ 操作,如下所示:
AtomicInteger count = new AtomicInteger(0);
System.out.println(count.getAndIncrement()); // 相当于i++该函数的伪代码如下所示:
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 来说是一条硬件指令,是原子的,这在上面代码中没有体现出来,但是要知道!伪代码如下所示:
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,也去执行取钱操作,但我浑然不知,假设 B 比 A 提前完成了扣款操作。与此同时朋友给我银行转账了 500 块钱,执行的是线程 C,导致 balance 变成了 1000,在极端情况下,A 最后才完成了 CAS 操作,判断相同,又扣了 500 块钱,相当于朋友的钱被银行吃了!

如何解决这个问题❓❓❓
上述问题主要是由既有加操作,又有减操作引起的,只要我们让程序只能进行加操作,就能避免这种情况!
因此引入了版本号机制,约定每次修改操作,都需要对版本号加一,即在进行 CAS 操作时,不仅要比较共享变量的值是否等于期望值,还需要比较版本号是否匹配。这样可以确保即使共享变量的值在中间被修改过,但由于版本号不匹配,CAS 操作依然会失败,从而避免了上述问题的出现。
synchronized 原理结合上面的锁策略,可以总结出 synchronized 具有以下特性(只考虑 JDK 1.8)

在底层实现上,synchronized 是通过 monitorenter 和 monitorexit 指令(字节码指令)来实现锁的获取和释放。
在 JVM 中,对象的锁状态可以在运行时在 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 之间逐级升级(不可降级)。

锁状态 | 特点 | 适用场景 |
|---|---|---|
无锁 | 默认状态,无同步需求 | 没有使用 synchronized |
偏向锁 | 只被一个线程使用过的锁 | 无线程竞争 |
轻量级锁 | 多线程并发,但竞争很小 | 多个线程交替执行同步块,对锁竞争小 |
重量级锁 | 多线程并发,且竞争严重 | 多个线程同时进入同步块,阻塞 |
偏向锁:不是真正的加锁,只是在对象头中标记锁信息为 "偏向锁",只有当其它线程也来竞争该锁的时候,才会取消偏向锁状态,转变为 "轻量级锁"。这样子做的好处有点像 "懒汉模式" 的延迟启动,尽量避免不必要的开锁开销,因为不一定真的有线程来竞争锁!
对象头结构 是
JVM用来管理对象运行时信息的元数据容器,每个Java对象在内存中分为三部分:
Header):存储锁状态、GC 分代年龄、哈希码等Instance Data):对象的字段值Padding):补齐内存对齐
偏向锁被第二个线程访问时会撤销并升级为轻量级锁,轻量级锁通过 CAS 以及自旋来代替线程阻塞,减少用户态与内核态切换的成本,步骤可以参考上面介绍 CAS 中实现自旋锁的那段伪代码,逻辑差不多!
如果竞争进一步激烈,自旋不能快速获取到锁状态,synchronized 就会自动转变为重量级锁,即采用内核提供的 mutex 来保证线程安全!步骤大概如下所示:
mutex 加锁操作,进入内核态锁消除是 JVM 的一种优化技术,当编译器和 JVM 检测到同步代码块实际上并不存在共享数据的竞争,则会直接移除对应的同步指令,提高效率!
举个例子,本身就是线程安全的 StringBuffer,可能被一些程序员用 synchronized 加锁保护起来,但压根没必要加锁,所以这些代码实际上都会被移除!
锁粗化是 JVM 针对高频次加锁场景的优化技术,通过合并相邻的同步块来减少锁竞争和上下文切换的开销。当编译器检测到多个连续的同步操作使用同一锁对象时(例如循环体内反复加锁或相邻代码段重复获取同一锁),会将多个细粒度的锁区域合并为一个更大的锁范围,从而将原本多次的加锁/解锁操作简化为单次操作。
举个很简单的例子,你有三个任务要向老板汇报,你分三次打电话过去汇报每个任务,和分一次去汇报三个任务的结果是一样的,但这个过程中显然后者更省力更高效!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。