Java 内存模型与 synchronized、volatile 深度解析
Java 内存模型(Java Memory Model,简称 JMM)是 Java 并发编程的基石。要彻底读懂 synchronized 和 volatile 这两大金刚,只有了解底层并发模型,懂得硬件级别的流水线与缓冲,才能游刃有余。
本篇将作为一份终极指南,从 JVM 内存的表层划分,一路深潜至字节码的 ACC_VOLATILE 烙印,再到 CPU 和操作系统的 Store Buffer 与对象头 Mark Word 变身,层层揭晓这背后的庞然大局。
1. 物理视角与逻辑抽象:JVM 运行时内存 vs JMM
在理解多线程之前,先要搞清楚 JVM 在运行时把物理内存分成了哪些区域。这些区域和 JMM 是两个维度的概念——JVM 内存划分描述的是"数据存在哪里",而 JMM 描述的是"线程如何安全地访问这些数据"。
1.1 JVM 运行时区域:“一栋办公大楼”
如果你把 JVM 想象成一栋巨大的办公大楼,那么它的内存就可以划分为这几个设施:
┌──────────────────────────────────────────────────────────┐
│ JVM = 一栋办公大楼 │
│ │
│ 📦 堆 (Heap) = 公共仓库(存放对象/数组,所有部门共用) │
│ 🗄️ 方法区 (Metaspace) = 档案室(公司类章程、静态变量、常量) │
│ │
│ 🪑 虚拟机栈 = 私人办公桌(每人一张,放自己局部变量的文件) │
│ 🏗️ 本地方法栈 = 外包工位(C/C++ 的 Native 施工队专用) │
│ 👆 程序计数器 (PC) = 手指指着大纲的那一行(记录执行到哪了) │
└──────────────────────────────────────────────────────────┘
在这个比喻中:
- 私有的东西(虚拟机栈、局部变量表等)别人碰不到:你在自己桌子上(方法内的
int temp)怎么折腾,都不会有并发问题。 - 公共区域(堆和方法区中)面临争抢:所以,JMM 只关心公共仓库(堆里对象实例字段)和档案室(方法区静态成员)的并发访问!
1.2 JMM 的横向抽象模型
为了屏蔽不同硬件系统的调度机制,JMM 提出了两层统一逻辑抽象:主内存(Main Memory)和工作内存(Working Memory)。
┌──────────────┐ ┌──────────────┐
│ 线程 A │ │ 线程 B │
│ ┌──────────┐ │ │ ┌──────────┐ │
│ │ 工作内存 │ │ │ │ 工作内存 │ │
│ └────┬─────┘ │ │ └────┬─────┘ │
└──────┼───────┘ └──────┼───────┘
Save │ ▲ Load Save │ ▲ Load
▼ │ ▼ │
┌───────────────────────────────┐
│ 主内存 │
│ (包含上面提到的堆对象与静态变量等)│
└───────────────────────────────┘
- 工作内存是个逻辑概念,它涵盖了 CPU 核心内部的寄存器、L1/L2 高速缓存等。线程不能直接跨越主内存读写,必须先将数据 Load 到工作内存。
为了规范主内存和工作内存的物资流转,JMM 设计了一套严密的"快递柜交互体系"——包含 8 种原子交互指令。
1.3 核心八大操作:主副内存的快递系统
JMM 定义了 8 种原子级操作。如果把主内存看作"中央发货库",工作内存看作"你家门口的快递柜",执行引擎看作"你本人":
读取方向(从发货库到你手里):
- read(读取):顺丰发货,从主内提取变量值。
- load(载入):包裹到达,放进工作内存(快递柜)作为副本。
- use(使用):你打开柜子取出,提供给执行引擎用以计算。
写入方向(打包给发货库): 4. assign(赋值):计算完毕,你把新值放进快递柜包裹。 5. store(存储):快递小哥取件,从工作内存传送到总线干道。 6. write(写入):仓库登记签收,存入主内存更新。
最高权限控制: 7. lock(锁定):锁住主内存某个值(给库房上黑锁),同时强行清空你本地快递柜,下次使用必须重新从主库同步最新件! 8. unlock(解锁):解除独占,并在解开前强行把修改的成果连夜走 store+write 刷回主库。
这 8 大操作被严格约束,例如 read/load 和 store/write 必须成对出现。它确保了跨线程通讯的坚实地基。
2. 硬件层面的深渊:并发 Bug 根源
理解了上层模型,我们必须往下扎根——为什么所有的编程语言都需要对抗这让人痛苦的并发恶龙?因为现代硬件架构存在两座大山。
2.1 速度剪刀差与可见性问题
CPU 性能是火箭,而内存是自行车。为了抹平这个差距,CPU 内部引入了 L1/L2 高速缓存。
CPU 1 CPU 2
┌───────────┐ ┌───────────┐
│ 寄存器/L1 │ │ 寄存器/L1 │ ◁ 各自的“工作缓存”
└─────┬─────┘ └─────┬─────┘
│ │
└─────────┬───────────┘
▼
┌───────────┐
│ 主内存 │ ◁ “系统总线”
└───────────┘
你以为这就是全部困境了?并不是。 哪怕后续有强悍的芯片级 MESI 缓存一致性协议,当一个核心修改了共享值,若要死等其它核心逐个响应“已标记失效”才能继续向下跑指令的话,由于存在网总线延迟,CPU 会被活活卡死退化回单核效率。
于是硬件工程师们丧心病狂地在核心内增设了:
- Store Buffer(写缓存区/发件箱):不等待其它人,刚改的变量全扔进去就往下跑指令。
- Invalidate Queue(失效列队/草稿箱):收到作废通知懒得理,堆在队列里事后统一作废。
这就导致了极度危险的硬件延迟(伪重排序):明明核心 A 修改了状态且物理上比 B 的读取发生得更早,但因为卡在 Buffer,核心 B 读到的依旧是不见天日的旧缓存。
2.2 as-if-serial 与三级重排序
为了榨干每一滴 CPU 性能,只要判定不存在上下行直接数据依赖,系统还会大洗牌指令顺序。JMM 允许这种现象存在,底层叫:as-if-serial 语义,即不管怎么重排序,只要单线程里的执行结果是对的就行。
但如果在多线程这可是灾难,重排分三道关卡:
- 编译器优化重排序:Javac 或 JIT 在编译代码时刻主动打乱前后的无关代码。
- 指令级并行重排序:现代 CPU 超标量流水线可以同时发射不相关的微指令。
- 内存系统重排序:上面提到的 Store Buffer 导致的视觉错乱(本已发起写入,但其它核还没看到,后续读取却在这期间返回了,造成乱序假象)。
3. 防线反击:JMM 的契约与内存屏障
3.1 程序员的定心丸:happens-before 规则
JMM 不想让 Java 开发去记那些复杂的指令发射系统,于是给出一道契约保证——happens-before。
它承诺只要两个操作之间存在 happens-before 关联,那么无论底层怎么作乱,前面的结果必须对后面可见,跨界重排序绝对被禁止。
这包含了八大条律(如单线程顺序规则、锁规则)。其中最常用的还包括传递性规则:
如果 A happens-before B,且 B happens-before C,则 A 必然 happens-before C。(这也是为何 volatile 能为前面的普通变量铺平路障的核心大招)
3.2 终极大杀器:CPU 内存屏障(Memory Barrier)
既然是 CPU Store Buffer 这些微结构闯的祸,那就必须由 CPU 自己填平。它们提供了汇编级的“内存屏障”。 屏障是一声咆哮:“立刻结束流水线乱排,清空待办列表!”。JMM 将其划定为四种通用屏障指令:
- LoadLoad 屏障(先看完,再接着读):必须清理完 Invalidate Queue 这类报废通知,保证后续读到的不再是垃圾旧值。
- StoreStore 屏障(先贴好,再写下一条):保证前面的改动已经挤出 Store Buffer 落入高速缓存,被全体窥探到。
- LoadStore 屏障(看明白了,再动手写):等待前置读取完成再进行写入发射。
- StoreLoad 屏障(雷霆万钧的重斧)⚡:必须强制把整个写缓存冲刷入最终存储,并在所有架构处理完失效通知后再去读!这几乎是一个**"全屏障" (Full Barrier)**。在 x86 架构中,经常是通过给寄存器指令加个
lock前缀(lock addl)变相达成的强效制止。
有了这些核武器在手,我们正式检阅主角:volatile。
4. 主角一:volatile 的灵魂解析(轻量防线)
4.1 深入字节码:它到底是什么标志位?
在深度排查或代码审查时经常容易踩坑:volatile 在字节码指令是不是用类似 volatile_putfield 之类的特殊指令?
完全不是!在字节码上普通访问和 volatile 的访问长得一模一样,都是 putfield/getfield。
它的秘密藏在访问标志位里(Access Flags):
// class 文件中的字段表(field_info)
flag 字段的 access_flags = 0x0040 (ACC_VOLATILE) ← 关键标记!
normal 字段的 access_flags = 0x0000
运行引擎发现这个目标有这个印记时,会在下行到机器码的那一刻进行拦截,并依据规则生生在目标前后塞入屏障!
4.2 JSR-133 与它的内存屏障布阵
在古早的历史(JDK 1.5 以前),volatile 只是宣称自身的屏障不能互逆,但普通变量能随便在它身前身后乱蹿翻越高墙。是之后的伟大协议 JSR-133(Java 内存模型重修) 赐予了它比肩锁一般的强悍排斥力。
我们来看看 JVM 是何等密集地下了一盘完整的读写两端四联屏障大网(时间线全景还原):
int a = 0; // 普通变量
volatile boolean ready = false; // volatile 旗帜
时间 │ 线程 A(写入端) │ 线程 B(读取端)
─────┼──────────────────────────────┼──────────────────────────────
t1 │执行 a = 42; 存入 Store Buffer │
│ │
t2 │遇到【StoreStore屏障】! │
│强制阻塞直到 a=42 真实刷入主系统 │
│ │
t3 │执行 volatile 写 ready = true │
│ │
t4 │遇到【StoreLoad屏障】! ⚡ │
│触发极重的停顿锁截(lock前缀引申)│
│全清Buffer, 迫使B方旧缓存报废 │
─────┼──────────────────────────────┼──────────────────────────────
t5 │ │ 执行 volatile 读 ready
│ │ 缓存失效,强迫从总内存夺到真值true
│ │
t6 │ │ 遇到【LoadLoad屏障】!
│ │ 被逼扫清旧的各种废弃通知收件箱
│ │
t7 │ │ 遇到【LoadStore屏障】!
│ │ (防后续误写)
│ │
t8 │ │ 执行读 a
│ │ 由于t6的影响,a的旧缓存也已被废,
│ │ 一定能去内存读到最新刷出:42
─────┼──────────────────────────────┼──────────────────────────────
结果 │ 写入全线生威! │ 全读尽收眼底,完美闭环!
4.3 为什么它不能保证原子性?
虽然能够看穿时空倒错,但 volatile 防不住多行微指令的被中断切割。比如常用来背锅的 count++。
在底层,这一句代码被无情切为了死局:
1: getfield <count> // 读取真实值(如0),虽然此时绝对真切,但这一步结束切出去了!
2: iconst_1
3: iadd // 在计算芯片内相加(得出1)
4: putfield <count> // volatile强刷回主存(写入1!)但别人可能早抢着写到了5并早已被覆盖。
解决方案:遇到计算累积,必须出动 AtomicInteger 通过底层提供的 CAS 进行对比硬卡位。
4.4 最佳使用场景(双检查锁定重排实录)
基于上述强横的“拦截带”表现,它最佳的用途是状态切换和一次性安全暴露(比如各种 init config 的暴露)。而最大的背书应用则是大名鼎鼎的 DCL 单例。
public class Singleton {
private static volatile Singleton instance; // 如果不加它,必坠深渊!
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 魔鬼在这里!
}
}
}
return instance;
}
}
这句实例化并非一气呵成:
- ① allocate():分配对象内存碎片
- ② initInstance():走构造函数,把成员属性充能塞实
- ③ instance = memory:指针绑定
由于 ② 和 ③ 在单线程无依赖,狡猾的 JIT 和 CPU 就可能将其重排为 ① ➡️ ③ ➡️ ②!
时间线 线程 A (实例化) 线程 B (获取实例)
│
t1 ① allocate() 分配内存
│
t2 ③ instance = memory 绑定指针 ━┓
│ (指令被重排在初始化前!) ┃
t3 ┣━━▶ 线程B通过外层 if (instance == null)
│ ┃ 因为指针早已不为空,直接拿到了 instance 地址!
t4 ┗━━▶ 线程B愉快拿着全废数据的“壳子”调用 ➡️ 触发NPE
│
t5 ② initInstance() 终于填完了真相...
▼
只要加上了 volatile。StoreStore 屏障就会在 ③ instance 前如铁板钉下死律:必定把前面初始化 ② 的东西悉数落锤主存完毕,才允许给指针交班。
5. 主角二:synchronized 的沉痛底盘(重量防线)
哪怕 volatile 把乱序防御做到了尽头,只要需要跨线程执行一系列组合动作并防竞争(原子性保障串行执行),唯一的真神就是它。
5.1 解析两副字节码面具
在 Java 代码里,synchronized 展现出了不同的底层烙印流派。
第一派:局部代码块
public void transfer() {
synchronized (this) { balance -= 10; }
}
被抽成字节码呈现出明确的哨兵:
monitorenter(持钥匙入死门)- 正常业务后
monitorexit(交钥匙出死门) - 编译器隐秘生成的暗门:只要是块,一定在最后利用隐形抛出段再次抛出个
monitorexit!(保证遇到严重报错死在里面时,绝对不会把门也带着吞噬死锁卡住!)
第二派:隐式烙印的方法级
如果挂在 public synchronized void method()。里头根本找不见 monitorenter。这取决于在方法总字段里会有一层 access_flags = ACC_SYNCHRONIZED。当引擎识别这东西,直接隐退内部寻找锁再入室。
5.2 大底层 C++:ObjectMonitor 排队之巅
不管是哪一种,最终都要在堆去争夺一个核心产物:ObjectMonitor。这内部是一套极度巧妙的抗拥挤缓冲区方案:
竞争与流转示意图:
┌──────────────────────────────────────────────┐
│ ObjectMonitor │
│ │
│ 【_owner】 │
│ ┌───── (正在执行的线程) ─────┐ │
│ │ │ │
│ ▼ ▼ │
│ 【_WaitSet】 【_EntryList】│
│ (休息室/wait态) (正式排队准备)│
│ │ ▲ │
│ │ │ │
│ └──────────┐ ┌─────┘ │
│ (notify唤醒) │ │ │
│ ▼ │ │
│ 【_cxq】───────┘ │
└───────────────── (竞争缓冲带) ───────────────┘
▲
│
(浩浩荡荡涌入的疯狂并发线程群)
_owner(王座):持有此物的真神才能执行。_cxq(CAS竞争通道):极妙的手笔!如果几万人同时去插队_EntryList那不又变成要加锁去锁外侧了么。于是做成极轻开销的单向 CAS 游离重挂队列。挂不上的兄弟自己原地绕。持有人走之后整条推给_EntryList。_WaitSet(长眠席):若是持锁时执行了本尊的this.wait()事件,退给别人,自己降生这排,直到被notify()捞进缓冲。
5.3 锁升级与 Mark Word 究极解密:10 个大汉抢一道门的微观战斗
要是次次都麻烦 ObjectMonitor 走内核态阻塞,系统早就卡瘫了。于是 JDK 1.6 引入了锁升级机制(偏向 → 轻量 → 重量级锁)。不可逆转。
这一切抢夺战的裁判,全靠每个 Java 对象天生自带的脑门印记:Mark Word(标记字)。在 64 位 JVM 架构中,它本身是一个压缩到了极致的 64 bit(8 字节)的元数据列。你可以把它理解为一个极其敏感的电子门牌,通过最后 2 个 bit 位的组合代表当前的门锁抗击打状态。
5.3.1 Mark Word 的内部结构图谱
64 bit Mark Word 在不同锁状态下的“七十二变”:
┌─────────────────────────────────────────────────────────────┐
│ 无锁状态 (01) │
├──────────────────┬─────────┬─────────┬───────┬──────────────┤
│ unused (25) │hashcode │ age(4) │ 0 │ 01 │
│ │ (31) │ │偏向位 │ 锁标志位 │
└──────────────────┴─────────┴─────────┴───────┴──────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 偏向锁状态 (01) │
├──────────────────────────┬────────┬────────┬───────┬────────┤
│ Thread ID (54) │epoch(2)│ age(4) │ 1 │ 01 │
│ (记录上一次进去的线程 ID) │ │ │偏向位 │锁标志位 │
└──────────────────────────┴────────┴────────┴───────┴────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 轻量级锁状态 (00) │
├───────────────────────────────────────────────────────┬─────┤
│ ptr_to_lock_record (62 bit 指针) │ 00 │
│ (直接指向抢到锁的那个兄弟自己栈帧里的 Lock Record 小纸条) │ │
└───────────────────────────────────────────────────────┴─────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 重量级锁状态 (10) │
├───────────────────────────────────────────────────────┬─────┤
│ ptr_to_ObjectMonitor (62 bit 指针) │ 10 │
│ (彻底放弃抵抗,交由堆内存里的重量级 ObjectMonitor 接管) │ │
└───────────────────────────────────────────────────────┴─────┘
5.3.2 究极推演:锁是怎么一步步被迫升级的?
在底层机制中,锁的升级实际上是一场**“尽量推迟向操作系统求救”**的性能保卫战。当一个门(Java 对象)被争抢时,它是如何随着竞争的加剧,被迫从极轻量的“偏向锁”一步步沦陷升级为“重量级锁”的呢?
第一步:偏向锁(Biased Lock)—— 只有你一个人,连门都不用锁(耗时约 1ns)
默认情况下,对象刚生下来是处于无锁可偏向状态的(Mark Word 标志位为 01)。
- 第一次造访:线程 A 来了,发现大门标志是
01,而且上面没写别人的名字。它就用硬件级的 CAS (Compare And Swap) 指令,花了 ~1ns 把大门上前 54 位改成自己的Thread ID。这就叫锁偏向了 A。 - 以后再来:A 第二次、第三次来,它根本不需要去抢什么锁。只要抬头看一眼门牌上写着自己的 ID,直接大摇大摆走进去。此时的性能快到极致,基本等于只做了一次
if(id == myId)的比较。
第二步:轻量级锁(Lightweight Lock)—— 竞争出现,大家兜圈子不睡觉(耗时约 10ns) 好景不长,线程 B 突然也赶到了!
- B 看了一眼门牌,发现最后两位是
01,但上面的Thread ID写的是线程 A 的名字,不是自己。 - B 的这一眼,宣告了“绝对安全”的打破!发生了竞争,这导致了偏向锁撤销。大家的共识变成了:“既然有别人也盯着这道门,我们谁都别想睡安稳觉了,全部换正式的机制!”
- 系统的裁判强行把门牌清空。此时 A 和 B(以及后面赶到的 C/D)大家一起退后一步。他们在各自的口袋(线程私有栈帧)里掏出了一张纸条,学名叫 Lock Record(锁记录)。
- 所有人一起发动 CAS 抢门,试图把刚才门牌上的标志改成
00,且把地址篡改成自己兜里那张小纸条的地址。 - 假如 B 抢赢了:B 兴奋地冲了进去。
- 没抢赢的 A 和 C 怎么办? 他们不会去死睡,而是赌 B 马上就出来。所以他们就在门外原地疯狂转圈圈(执行底层的空转 while 循环),一边跑圈一边继续用 CAS 拼命捅那个门锁试图覆盖。这就叫做自旋锁(Spin Lock)。因为没有调用操作系统让线程挂起(挂起的上下文切换极费时间),所以整体响应依然极快。
第三步:重量级锁(Heavyweight Lock)—— 场面失控 A 和 C 在门外疯狂跑了几十圈(自旋次数到了极值,也就是自适应自旋失败)。他们发现 B 这个家伙在里面是在干大活(比如执行极慢的网络调用),迟迟不出来。
- 场面彻底失控:如果任由 A、C 继续这么跑下去,他们俩会把这台机器的 CPU 完全占满烧干,别的程序连运行的机会都没有了。
- 锁膨胀发动(Inflation):系统终于忍无可忍,强制没收了他们的行动自由。
- JVM 连夜在旁边的堆里修了一座庞大的保安亭(名为
ObjectMonitor,也就是底层 C++ 实现的管程)。并且强行把刚才那扇大门标志位改成10,指针指向这座保安亭。 - 底层怎么排队?(
_cxq入列):跑圈累个半死的 A 和 C,被全权转交给了操作系统进入内核态深度睡眠前,会被塞进底层的单向并发列表_cxq(竞争队列)。这个队列非常奇葩,它通过 CAS 插入,而且是头插法(后进排在前面)。 - 这个从用户态堕入内核态的消耗是极其巨大的,大约在一万纳秒级别。这就是传说中的重量级锁。
- 释放与唤醒: 最后 B 终于开门出来了。他一看门成了重锁,只能去地下室发出唤醒指令。他会将
_cxq里的人推入正式的等候室_EntryList中,并唤醒候选人 A 先冲。 - 这是公平锁吗?绝对的非法平锁!
- 首先,
_cxq的头插法让最后来的 C 反而排在先来的 A 前面。 - 更残酷的非公平在于野蛮插队:当大门刚好交接的一瞬间,如果正好有一个刚诞生的线程 E 跑过来,E 根本不走大门去排队,而是直接对门头来一发冲撞!由于 B 刚把锁放下,E 瞬间就把大门据为己有。而刚刚被 B 千辛万苦唤醒、正揉着眼睛从地下室爬出来的老实人 A 看呆了,只能叹口气,重新走回地下室接着睡。这就是
synchronized极其霸道非公平、但也因此换来了极高吞吐特性 的原因。
- 首先,
【小结:不可退让的底线】
这也是为什么锁是**“不可逆的一步步膨胀”**:
它其实是由乐观到悲观的妥协过程:认为只有自己(偏向) ➡️ 认为别人马上出来(自旋) ➡️ 认命绝望只能去深度睡眠排队(重量)。每一次升级,都是极其不得已地向更重的操作系统机制去“求援”。
6. 附加篇:Java 宏观锁分类演播室
除了底层具体的锁实现,在实际架构设计中,我们还常常听到一系列高频的“锁阵营”名词。它们更多代表的是设计思想与策略,而不是某一段死板的代码:
6.1 悲观锁 vs 乐观锁 (Pessimistic vs Optimistic)
- 悲观锁:总觉得别人会来抢。拿到门票之前绝不干活,操作时全程物理封锁。
- 代表:
synchronized、ReentrantLock等所有互斥锁。 - 场景:写操作极多,碰撞激烈的红海战场。
- 代表:
- 乐观锁:觉得世界是和平的,一般不会有人和我抢。不上门锁,直接去拿数据修改。仅在我准备最终更新的那一刻,再去看下有没有人在我修改期间偷改过原数据;如果发生了偷改,我就从头重试(或放弃)。
- 代表:各种
CAS(Compare And Swap)操作手段,例如底层Unsafe.compareAndSwapInt、高层的AtomicInteger。 - 场景:读多写少的轻量级面板。
- 代表:各种
6.2 自旋锁 vs 阻塞锁 (Spin vs Blocking)
- 自旋锁:争夺失败时,不马上躺下睡觉,而是在旁边拼命狂奔绕圈空转(原地
while(true)浪费 CPU 循环着等)。- 代表:轻量级锁等待时的方式、各类基于 CAS 的重试死循环。
- 优点:如果对方锁释放得极快,能完美避开系统 OS 内核态挂起这把性能砍刀;缺点:如果对方霸位不走,你会把宿主机的 CPU 算力给活活干枯焦。
- 阻塞锁:争锁失败时,绝不浪费算力,当即放弃占用 CPU 时间片,沉睡挂起,等待被底层的宿管大爷唤醒。
- 代表:重量级锁
ObjectMonitor.park()。
- 代表:重量级锁
6.3 公平锁 vs 非公平锁 (Fair vs Unfair)
- 公平锁:讲规矩。争锁必须去排队,谁先来挂起等待的,谁先拿到下一把钥匙。
- 代表:
ReentrantLock(true)。 - 特点:杜绝了线程饿死的问题;但由于需要精确维持高成本的阻塞唤醒队列,整体吞吐性能处于劣势。
- 代表:
- 非公平锁:土匪逻辑,蛮横霸道。刚赶到门前时,若发现门正在交接,管他人排不排队,它直接抄起工具用 CAS 猛冲撞门,撞开了就插队独用。撞不开才去老实排队。
- 代表:
synchronized(天生的彻头彻尾非公平土匪锁)、默认配置的ReentrantLock()。 - 特点:由于存在极大概率的“省去一轮线程阻塞与恢复唤醒的时间”,系统吞吐性能极高!
- 代表:
6.4 可重入锁 (ReentrantLock)
- 概念:同一个角色在拿到某房门的大锁后,如果在该房间内还要执行要求提供同样房间钥匙的其他方法步骤,它依然会被放行,而不是自己把自己当作外人直接死缩挡死在里面。
- 代表:
synchronized本身是完全可重入的(依赖 ObjectMonitor 的_recursions深度计数属性加减)、ReentrantLock。 - 机制:大门检查持印人的线程 ID,若还是同一个人,直接计数层层加一即可放行,退出一层减一层。
- 代表:
这四大阵营就是常常缠绕在我们并发架构设计中的宏观兵法心诀。
7. 归总:战地红黑榜
把两大神物的核心解剖一遍后,可以给出一份明了且致命的技术决策底牌以作收束:
| 对比横切阵列 | volatile |
synchronized |
|---|---|---|
| 解决痛点边界 | 仅可见性、有序性(防乱飞) | 确保一切:加上最难能的 原子串行性 |
| 底层硬件绝活 | 多层交织的 StoreLoad 屏障迫使清缓冲 |
ObjectMonitor C++的流转机制与唤醒调用 |
| 历史优化痕迹 | JDK 1.5 JSR-133 翻身赋能阻排一切 | JDK 1.6 推锁升级梯队救命,JDK15 挥泪斩偏向 |
| 性能极限拖累 | 无上下文切换、无内核堕退、轻捷灵敏 | 一旦轻量级扛不住就大开耗能之血闸挂退内核态 |
| 最佳战斗定位 | 监控配置位翻转、非强累积类状态位、DCL 截断。尽早应用。 | 长串大复合计算防割裂、高竞争复杂操作链、重型更新。 |
当你越明了底层为何引入 Buffer 为何造出 CAS 和 Monitor 轮盘,JMM 这种极具张力的妥协承诺对于你而言,就不再仅仅是写代码加的个小后缀,而是能洞见整个虚拟器大厦从源码直指硬件的运行哲学。