java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java锁机制

Java锁机制完整学习笔记(附详细代码)

作者:井九111

Java中的锁机制是并发编程的核心工具之一,通过合理使用锁、、读写锁和自旋锁,开发者可以确保多线程环境下的数据一致性和程序正确性,这篇文章主要介绍了Java锁机制完整学习的相关资料,需要的朋友可以参考下

一、锁的起源:为什么需要锁?

1.1 问题场景

int count = 0;

// 线程A 和 线程B 同时执行
count++;

count++ 在CPU层面是三步操作:

1. 读取 count 的值到寄存器
2. 寄存器的值 +1
3. 把结果写回内存

1.2 并发问题

线程A: 读取count=0
线程B: 读取count=0  
线程A: 计算0+1=1
线程B: 计算0+1=1
线程A: 写入count=1
线程B: 写入count=1

结果:count=1,而不是期望的2

核心问题:如何让一组操作不可分割地执行?

二、volatile 能解决吗?

2.1 答案:不能

volatile int count = 0;

// 依然有问题
count++;

2.2 volatile 解决的是

特性说明
可见性一个线程修改后,其他线程立即看到
有序性禁止指令重排序

不解决原子性 —— "读-改-写"之间,别的线程还是可以插进来。

三、CAS:硬件级别的原子操作

3.1 CPU 提供的能力

CPU 提供特殊指令(x86 的 CMPXCHG):

比较内存值是否等于预期值
如果相等,就把新值写入
整个过程不可被打断

这就是 Compare-And-Swap

3.2 技术栈关系

CPU 提供:CAS指令(硬件能力,一直都有)
    ↓
JVM 实现:Unsafe类(Java访问底层能力的桥梁)
    ↓
JDK 封装:LockSupport、Atomic类等

四、Java 锁的演进历程

4.1 演进时间线

JDK 1.0:synchronized(重量级锁)
JDK 1.5:ReentrantLock、CAS、AQS
JDK 1.6:锁升级(偏向锁、轻量级锁)
JDK 1.8:StampedLock(乐观读)
JDK 15: 废弃偏向锁
JDK 21: 虚拟线程,synchronized 有 Pinning 问题
JDK 24+:修复 synchronized 支持虚拟线程

五、JDK 1.0:synchronized 重量级锁

5.1 实现方式

synchronized (obj) {
    count++;
}

早期实现:

5.2 内核态切换的开销

为什么需要切换?

用户态(User Mode):Java程序运行的地方,权限受限
内核态(Kernel Mode):操作系统运行的地方,权限最高

阻塞线程、唤醒线程等操作需要操作系统介入,必须切换。

切换时发生了什么?

1. 保存现场
   - 当前所有CPU寄存器的值
   - 程序计数器(执行到哪了)
   - 栈指针
   → 全部存到内存

2. 切换栈
   - 从用户栈切到内核栈

3. 执行内核代码
   - 内核的代码和数据被加载到CPU缓存
   - 程序的代码和数据可能被挤出缓存

4. 返回用户态
   - 恢复之前保存的所有寄存器
   - 切回用户栈
   - 重新加载程序的代码和数据到缓存

开销量化

CPU缓存访问:约 1-10 纳秒
内存访问:约 100 纳秒

一次 count++:        约 几个 CPU周期
一次内核态切换:      约 几千~几万个 CPU周期

问题:锁本身的开销 >> 临界区代码的开销

六、JDK 1.5:ReentrantLock 与 AQS

6.1 改进思路

先用 CAS 自旋几次(用户态,不切换)
    ↓
还拿不到?再调用 park 阻塞(才进内核态)

能不阻塞就不阻塞。

6.2 对比

早期 synchronized:
拿不到 → 立即阻塞 → 进内核态

ReentrantLock:
拿不到 → 先自旋 → 还不行再阻塞

特性Thread.sleep()Object.wait()LockSupport.park()
锁释放不释放任何锁释放 synchronized 监视器锁不直接操作锁
唤醒方式超时自动唤醒notify()/notifyAll()unpark(Thread)
唤醒目标只能唤醒自己随机或全部精确唤醒指定线程
使用位置任何地方必须在 synchronized 块内任何地方

6.4 AQS 核心结构

AQS = state 状态变量 + CLH 双向队列

CLH:Craig, Landin, and Hagersten Queue,三个发明者的名字。

七、JDK 1.6:synchronized 锁升级

7.1 核心思想

根据竞争激烈程度,逐步升级:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

7.2 对象头 Mark Word

每个 Java 对象在内存中都有对象头,其中 Mark Word(64位)存储锁信息:

锁标志位(最后2位):
  01 → 无锁 或 偏向锁(看倒数第3位区分)
  00 → 轻量级锁
  10 → 重量级锁

详细结构:
┌─────────────────────────────────────────────────┐
│ 无锁:   │ hashCode │ 分代年龄 │ 0 │ 01 │       │
│ 偏向锁: │ 线程ID │ epoch │ 分代年龄 │ 1 │ 01 │  │
│ 轻量级: │ 指向栈中锁记录的指针           │ 00 │  │
│ 重量级: │ 指向Monitor对象的指针          │ 10 │  │
└─────────────────────────────────────────────────┘

7.3 偏向锁

场景:只有一个线程反复访问

// 线程A 第一次进入
synchronized (lock) {
    count++;
}
// 对象头写入线程A的ID

// 线程A 再次进入
synchronized (lock) {
    count++;
}
// 检查线程ID == A?直接进入,零开销

特点:

7.4 轻量级锁

场景:多个线程交替访问,无激烈竞争

触发条件: 第二个线程来访问时

// 线程A 进入
synchronized (lock) {
    count++;
}
// CAS 修改对象头,指向A的锁记录

// 线程A 离开
// CAS 恢复对象头,还原原来的Mark Word

特点:

7.5 重量级锁

场景:CAS 自旋多次仍失败,竞争激烈

Monitor 结构:

┌─────────────────────────┐
│  Owner: 当前持有锁的线程   │
│  EntryList: 阻塞等待的线程 │  ← 抢锁失败的线程排队
│  WaitSet: 调用wait()的线程 │
└─────────────────────────┘

7.6 锁升级流程图

lock 对象刚创建
    │
    ▼
  无锁(0 01)
    │
    │ 线程A第一次访问
    ▼
偏向锁(1 01)── 对象头记录线程A的ID
    │
    │ 线程B来访问
    ▼
轻量级锁(00)── CAS自旋竞争
    │
    │ 自旋失败,竞争激烈
    ▼
重量级锁(10)── 阻塞等待,进内核态

7.7 三种锁的开销对比

偏向锁:  比较ID → 进入          (一次比较)
轻量级锁:CAS修改指针 → 进入      (一次原子操作)
重量级锁:系统调用 → 可能阻塞     (内核态切换)

7.8 锁升级的特点

1. 单向升级,不会降级
2. 一旦出现过竞争,JVM认为后续还可能有竞争

八、JDK 15:废弃偏向锁

8.1 原因

现代应用并发程度高,单线程访问场景少
偏向锁撤销的开销 > 它带来的收益

8.2 变化

锁升级变成:

无锁 → 轻量级锁 → 重量级锁

跳过了偏向锁

九、读写锁

9.1 Java 读写锁(ReentrantReadWriteLock)

存储位置:AQS 的 state 变量(32位 int)

┌─────────────────┬─────────────────┐
│  高16位:读锁数量  │  低16位:写锁数量  │
└─────────────────┴─────────────────┘

9.2 Java 与数据库锁对应关系

Java数据库
读锁(Read Lock)共享锁(S锁)
写锁(Write Lock)排他锁(X锁)

9.3 兼容矩阵

        读锁      写锁
读锁    兼容 ✓    冲突 ✗
写锁    冲突 ✗    冲突 ✗

核心:读读不冲突,其他都冲突

十、数据库锁

10.1 意向锁(Intention Lock)

解决的问题:

事务A:锁住某一行(行级X锁)
事务B:想锁整张表(表级X锁)
    ↓
事务B 需要确保表里没有任何行被锁住
    ↓
没有意向锁:遍历100万行检查
有意向锁:检查表级标记,直接判断

工作流程:

事务A:
  1. 先给表加 IX锁(意向排他锁)
  2. 再给行加 X锁

事务B 想加表锁:
  1. 检查表级别有没有意向锁
  2. 发现有 IX锁 → 直接等待

意向锁兼容矩阵:

        IS    IX    S     X
IS      ✓     ✓     ✓     ✗
IX      ✓     ✓     ✗     ✗
S       ✓     ✗     ✓     ✗
X       ✗     ✗     ✗     ✗

10.2 粒度锁

解决的问题:幻读

-- 表里有 id = 1, 5, 10 三行

-- 事务A
SELECT * FROM users WHERE id > 3 AND id < 8;  -- 结果:id = 5

-- 事务B 插入新行
INSERT INTO users (id) VALUES (6);
COMMIT;

-- 事务A 再查一次
SELECT * FROM users WHERE id > 3 AND id < 8;  -- 结果:id = 5, 6  ← 幻读

三种锁:

锁类型说明作用
记录锁(Record Lock)锁具体的行防止修改/删除
间隙锁(Gap Lock)锁行之间的空隙防止插入
临键锁(Next-Key Lock)记录锁 + 间隙锁防止幻读

间隙示例:

表里有 id = 1, 5, 10

间隙:(-∞, 1)  (1, 5)  (5, 10)  (10, +∞)

查询 id > 3 AND id < 8 时:
锁住间隙 (1, 5) 和 (5, 10)
    ↓
插入 id = 6 被阻塞

十一、StampedLock(JDK 1.8)

11.1 解决的问题

ReentrantReadWriteLock 问题:
1. 读锁和写锁互斥,大量读时写线程饥饿
2. 即使只是读,也要加锁

11.2 乐观读

StampedLock lock = new StampedLock();

long stamp = lock.tryOptimisticRead();  // 获取版本号,不加锁
int x = this.x;  // 读数据
int y = this.y;

if (!lock.validate(stamp)) {  // 验证版本号
    // 版本变了,升级为悲观读锁
    stamp = lock.readLock();
    try {
        x = this.x;
        y = this.y;
    } finally {
        lock.unlockRead(stamp);
    }
}

11.3 三种模式

模式方法说明
乐观读tryOptimisticRead()不加锁,性能最好
悲观读readLock()和读写锁一样
写锁writeLock()排他

11.4 对比

ReentrantReadWriteLockStampedLock
乐观读
可重入
支持Condition

十二、公平锁 vs 非公平锁

类型说明性能
公平锁严格按队列顺序,不允许插队较低
非公平锁允许插队,谁抢到谁用较高

ReentrantLock 默认是非公平的。

十三、可重入锁

概念: 同一线程可以多次获取同一把锁

实现原理: 判断锁的持有者是否是当前线程

synchronized (lock) {     // 第一次获取
    synchronized (lock) { // 同一线程再次获取,允许
        // ...
    }
}

作用: 避免同一线程自己把自己锁死

十四、JDK 21+:虚拟线程与 Pinning 问题

14.1 虚拟线程原理

平台线程(OS线程):重量级,数量有限
虚拟线程:轻量级,可以创建百万个

┌─────────────────────────────────────┐
│  平台线程(载体线程)                  │
│  ┌─────┐ ┌─────┐ ┌─────┐            │
│  │虚拟1│ │虚拟2│ │虚拟3│  轮流执行    │
│  └─────┘ └─────┘ └─────┘            │
└─────────────────────────────────────┘

正常情况:

虚拟线程1 执行中,遇到阻塞
    ↓
JVM 把虚拟线程1 从平台线程上"卸载"
    ↓
平台线程去执行虚拟线程2
    ↓
虚拟线程1 的阻塞完成后,再"挂载"回来

14.2 Pinning 问题

synchronized (lock) {
    httpClient.send(request);  // 网络IO阻塞
}

问题流程:

虚拟线程进入 synchronized 块
    ↓
Monitor(监视器)记录在平台线程的栈帧上
    ↓
遇到 IO 阻塞,想卸载虚拟线程
    ↓
但 Monitor 信息和平台线程绑定,无法卸载
    ↓
虚拟线程"钉住"了平台线程
    ↓
平台线程只能傻等

14.3 为什么 ReentrantLock 没这个问题?

ReentrantLock 用 Java 代码实现(AQS)
    ↓
锁状态存在堆内存的 state 变量中
    ↓
不依赖平台线程的栈
    ↓
虚拟线程可以正常卸载

14.4 JDK 24 修复(JEP 491)

重新实现 synchronized 的底层机制
    ↓
Monitor 信息从栈上移到堆上
    ↓
不再和平台线程绑定
    ↓
虚拟线程可以正常卸载

对,你说得很准确。我更新一下总结:

十五、总结:锁的本质

两种实现路径

锁类型争抢目标存储位置
synchronized对象头 Mark Word对象内存布局
ReentrantLock / StampedLockstate 状态变量AQS 堆内存

核心机制

synchronized:
    CAS 修改对象头 → 成功则拿到锁

JUC 锁:
    CAS 修改 state 变量 → 成功则拿到锁

演进思路

能不阻塞就不阻塞
能在用户态解决就不进内核态
根据竞争程度选择合适的锁

总结

到此这篇关于Java锁机制的文章就介绍到这了,更多相关Java锁机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文