还没写完呢
在这之前先补充一些知识吧,方便后续相关知识的理解

一、内存中的对象长什么样子?

一张图说清楚吧。

image-20250514222923669

堆中的java对象只有数据部分,对象中的数据含义,要结合方法区中的class才能明了。

堆中的对象结构包括三部分:对象头、实例数据、填充数据。这里主要是见一下对象头部分,对象头可以说是一个对象的身份证了。

对象头的构成如下:

1
2
3
4
+------------------+------------------+------------------+------------------+
| Mark Word | Klass Pointer | [Array Length] | Instance Data |
+------------------+------------------+------------------+------------------+
| 64 bits | 32/64 bits | 32 bits | variable |

数组的对象头构成如下:

1
2
3
4
5
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
  • 普通对象:Mark Word + Klass Pointer 数组对象:额外包含4字节数组长度

MarkWord的结构如下:

image-20250514225913544

简单先看到这里。这篇博客的主菜是锁机制。

好了,正式开始介绍锁机制。

二、锁机制。

现代计算机系统中,锁的实现本质上是硬件指令与软件逻辑的协同产物。在x86架构中,LOCK前缀指令通过锁总线或缓存行来实现原子操作,而ARM架构的LDREX/STREX指令对则采用独占监视器机制。这些硬件特性构成了Java锁机制的物理基础。
——-摘自网络,都是中文,组成一起感觉就有点看不懂了。😵😵😵

2.1锁的本质:解决三大并发问题

  • 原子性:防止指令交错导致的中间状态(如i++的非原子操作)
  • 有序性:通过内存屏障强制刷新缓存(volatile变量的写-读建立happens-before关系)
  • 可见性:阻止指令重排序(单线程语义下的as-if-serial vs 多线程环境下的真实执行顺序)

2.2锁的分类

  • 悲观锁:synchronized、Reentrant Lock
  • 乐观锁:CAS、版本号控制。

三、慢慢剥开悲观锁!

3.1Monitor
在介绍悲观锁之前,先说一下Monitor,synchronized给对象上锁时,会把这个对象跟OS提供的一个Monitor相关联。每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。

image-20250514232319329

注意:synchronized 必须是进入同一个对象的 monitor 才有上述的效果,不加 synchronized 的对象不会关联监视器,不遵从以上规则
你也可以看这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────────────┐
│ Monitor │
│ ┌──────────────┐ │
│ │ Entry Set │←── 竞争锁失败的线程
│ └──────────────┘ │
│ ┌──────────────┐ │
│ │ Owner │──→ 当前持有锁的线程
│ └──────────────┘ │
│ ┌──────────────┐ │
│ │ Wait Set │←── 调用wait()的线程
│ └──────────────┘ │
└──────────────────┘

在 Hotspot 虚拟机中,Monitor 由 Object Monitor 实现

1
2
3
4
5
6
7
ObjectMonitor() {
_count = 0; // 记录线程获取锁的次数
_owner = NULL; // 指向持有ObjectMonitor对象的线程
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_cxq = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
}
  • _owner:当前持有 Object Monitor 的线程,初始值为 null,表示没有线程持有锁。线程成功获取锁后,该值更新为线程 ID,释放锁后重置为 null。
  • _count:记录当前线程获取锁的次数(可重入锁),每次成功加锁 _count + 1,释放锁 _count - 1
  • _Wait Set:等待队列,调用 wait() 方法后,线程会释放锁,并加入 _Wait Set,进入 WAITING 状态,等待 notify() 唤醒。
  • _cxq:阻塞队列,用于存放刚进入 Monitor 的线程(还未进入 _Entry List)。
  • _Entry List:竞争队列,所有等待获取锁的线程(BLOCKED 状态)会进入 _Entry List,等待锁释放后竞争执行权。
1
2
3
4
5
6
7
8
9
+----------------------+
| ObjectMonitor |
| ---------------- |
| _owner = Thread-1 | // 当前持有锁的线程
| _count = 1 | // 线程获取锁的次数
| _WaitSet -> T3,T4 | // 执行 wait() 的线程
| _EntryList -> T2,T5| // 竞争锁的线程
| _cxq -> T6,T7 | // 新进入的线程
+----------------------+

3.2 悲观锁☞synchronized

synchronized 依赖 JVM 内部的 Monitor 对象来实现线程同步。使用的时候不用手动去 lock 和 unlock,JVM 会自动加锁和解锁。

synchronized 加锁代码块时,JVM 会通过 monitorentermonitorexit 两个指令来实现同步

  • monitorenter 表示线程正在尝试获取 lock 对象的 Monitor
  • monitorexit表示线程执行完了同步代码块,正在释放锁

看一下代码

1
2
3
4
5
6
7
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}

对应的部分字节码对应如下:

image-20250514233116233

注意:方法级别的 synchronized 不会在字节码指令中有所体现。

3.3synchronized的锁升级过程:
①偏向锁:JDK15之后,偏向锁被废弃了,那我这里就不多赘述了。
至于为什么被删除?问了下AI:偏向锁被废弃是因为它在现代多核 CPU 和高并发场景下性能提升不明显,反而增加了虚拟机的复杂性和维护成本。
②轻量级锁:如果一个对象虽然有多个线程要加锁,蛋加锁的时间是错开的(也就是没有竞争),那么可以室友轻量级锁来优化。存在竞争时呢,轻量级锁就会变成重量级锁。轻量级锁锁对于使用者时透明的,即语法依然是synchronized。
看下轻量锁的过程:

  • 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Wordimage-20250515000403540

  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
    image-20250515000437794

  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下image-20250515000520516

  • 如果 CAS 失败,有两种情况
    **·**如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    **·**如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数image-20250515000634566

​ - 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一image-20250515000713063

​ 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头,成功,则解锁成功。失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
​ ③锁膨胀
如果尝试加锁轻量锁的过程中,CAS操作无法成功,这时就是有其它线程为此对象先加上了轻量锁,现在冲突了,需要进行锁膨胀,将轻量锁变为重量锁。
​ 线程1加锁失败之后就会去为这个线程申请Monitor锁,该线程进入Monitor的EntryListBLOCKED,当线程0退出同步块解锁时,使用CAS将Mark Word的值恢复给对象头,恢复失败的话