云计算百科
云计算领域专业知识百科平台

「JUC」线程安全与管程机制:Synchronized 原理与等待唤醒机制

引言:“在单线程的世界里,代码的执行就像一条笔直的高速公路,可预测且安全。但当我们踏入多线程的领地,共享资源就成了一个危机四伏的十字路口。如果没有红绿灯(同步机制),车辆(线程)的横冲直撞必然导致车祸(线程安全问题)。

本文将带你从最基本的线程安全现象入手,一路向下硬核剖析 JVM 是如何通过 Synchronized 和 Monitor 管程机制来维持秩序的;同时,我们也会探讨线程之间如何优雅地通信(Wait/Notify 与 Park/Unpark),并结合实际的并发设计模式与死锁排查,帮你真正驯服 Java 并发这匹野马。”


一、并发安全的危机与 Synchronized 初探

在单线程的世界里,代码的执行就像一条笔直的高速公路,可预测且安全。但当我们踏入多线程的领地,如果没有“红绿灯”和交警,车辆的横冲直撞必然导致严重的车祸。

🟢 临界区与竞态条件:车祸是怎么发生的?

简单来说,临界区就是一段“同时只能让一个线程执行”的代码,而这部分代码通常在操作共享资源。

想象一下商场里的单人洗手间(临界资源),如果门上没有锁,两个顾客(线程)同时冲进去,必然会发生非常尴尬的“冲突”。在程序中,当多个线程同时进入临界区,由于 CPU 线程调度是随机的,代码执行的先后顺序无法预测,最终导致数据错乱,这就是竞态条件。

一句话总结: 多个线程只读共享变量没问题,但一旦多线程同时读写共享变量且指令发生交错, BUG 就诞生了。

🟡 Synchronized 初探:最朴素的“红绿灯”

为了避免竞态条件,Java 提供了最简单粗暴的阻塞式解决方案:synchronized 关键字。它的核心思想是“对象锁”——线程进入临界区前必须先拿到钥匙,没拿到钥匙的线程只能在门外干等(阻塞)。

在实际的后端业务开发中(例如在做苍穹外卖项目时,处理多个骑手同时抢同一单的并发场景),我们最常使用以下三种加锁姿势。这里的核心考点只有一个:你锁的到底是谁?

Java

public class OrderService {
// 1. 同步代码块(锁自定义对象):粒度最细,实战建议首选
private final Object lock = new Object();
public void takeOrder1() {
synchronized(lock) {
// 只有拿到 lock 对象锁的线程,才能进来接单
}
}

// 2. 实例方法(默认锁 this):锁住的是当前正在调用该方法的 OrderService 实例
public synchronized void takeOrder2() {
// 临界区代码…
}

// 3. 静态方法(默认锁 Class):锁住的是整个 OrderService 类,属于降维打击
public static synchronized void takeOrder3() {
// 临界区代码…
}
}

🟡 经典检验:破局所谓的“线程八锁”

在面试大厂时,面试官特别喜欢甩出一堆看似花里胡哨的代码让你判断是否线程安全,这也就是经典的“线程八锁”笔试题。

其实根本不需要死记硬背那八种情况,破局的核心心法只有一句:多线程竞争的,到底是不是同一把锁?

我们直接看反差最大的两个极端场景:

场景 A:跨频道聊天(绝对不安全)

Java

OrderService s1 = new OrderService();
OrderService s2 = new OrderService();
// 骑手 A 调用 s1.takeOrder2() -> 拿的是 s1 的锁
// 骑手 B 调用 s2.takeOrder2() -> 拿的是 s2 的锁

结果: 并发乱套。因为它们拿的是两把完全不同的锁(不同的 this 实例),互相根本管不着对方,防线形同虚设。

场景 B:降维打击(绝对安全)

Java

OrderService s1 = new OrderService();
OrderService s2 = new OrderService();
// 骑手 A 调用 s1.takeOrder3() -> 拿的是 OrderService.class 锁
// 骑手 B 调用 s2.takeOrder3() -> 拿的是 OrderService.class 锁

结果: 完美同步。因为方法加了 static,锁对象变成了类的 Class 字节码对象。无论你 new 出多少个 OrderService 实例,在 JVM 中 Class 对象是全局唯一的。所有线程抢的都是同一把“最高级别的类锁”,自然规规矩矩。

二、深入 JVM 腹地——Monitor 管程与 Synchronized 底层进化史

在上一次的分享中,我们把 synchronized 比作了保护临界区的“红绿灯”。但你有没有想过,这个“红绿灯”在 Java 虚拟机里到底长什么样?它是凭什么拦住其他线程的?

今天,我们就潜入 JVM 的腹地,拆解这把锁的底层逻辑。

🔴 [红灯区] 对象头与 Monitor:锁的物理形态

大厂面试喜欢抠细节:你给一个对象加锁,这个锁的信息到底存在哪里? 答案就在**对象头(Object Header)**里。

在 64 位 JVM 中,每个 Java 对象都有一个对象头,其中最核心的部分叫 Mark Word。你可以把它理解为对象的“身份证”,里面记录了哈希码、GC 分代年龄,以及最重要的——锁标志位。

当我们使用 synchronized 加上重量级锁时,Mark Word 的状态会被修改,它的指针会指向一个叫做 Monitor(管程/监视器) 的对象。Monitor 就是真正维持并发秩序的核心大脑。

Monitor 内部有三个至关重要的组件:

  • Owner(主人):当前持有锁的线程。同一时刻只能有一个。
  • EntryList(等待走廊):双向链表。那些没抢到锁的线程,全都被扔到这里阻塞排队。
  • WaitSet(休息室):条件不满足,主动调用 wait() 放弃锁的线程在这里休息(下一章细讲)。
  • 💡面试官: 假设线程 A 拿到了锁,正在执行。此时线程 B 和 C 来了,发现 Monitor 的 Owner 不是自己,它们会经历什么? 回答: 它们会进入 Monitor 的 EntryList,并且线程状态从 RUNNABLE 转变为 BLOCKED(阻塞),此时它们会交出 CPU 执行权,处于挂起状态。当线程 A 执行完同步代码块,会将 Owner 置为空,并唤醒 EntryList 中的线程。注意,Java 中的 synchronized 唤醒是非公平的,B 和 C 谁抢到算谁的,甚至如果此时刚巧来了一个线程 D,D 也有可能直接插队抢走。

    🟡 [黄灯区] 字节码视角:双保险的退出机制

    既然锁是靠 Monitor 控制的,那代码是怎么和 Monitor 关联起来的?我们来看一段极简的同步块编译后的字节码:

    Java

    Object lock = new Object();
    synchronized (lock) {
    System.out.println("ok");
    }

    在底层字节码中,你会看到两个核心指令:

    • monitorenter:尝试将 lock 对象的 Mark Word 指向 Monitor,并将 Owner 设为自己。
    • monitorexit:重置 Mark Word,唤醒 EntryList。

    为什么会有两个 **monitorexit**? 很简单,防死锁。第一个 monitorexit 是正常走完同步块释放锁;第二个是编译器通过异常表(Exception table)隐式加入的。这就保证了哪怕你的代码在同步块里抛了异常崩溃了,JVM 也能兜底帮你把锁释放掉,不至于让其他排队的线程等到天荒地老。

    🔴 [红灯区] 锁升级全过程:JVM 的克制与进化

    早期的 Java 中,synchronized 是无脑的重量级锁(直接上 Monitor)。但申请重量级锁需要调用操作系统的 Mutex Lock,这会导致用户态到内核态的切换,极为消耗性能。

    JVM 团队后来发现了一个规律:在真实业务中,大多数情况下根本就没有多线程竞争,或者竞争的时间极短。 为了这点概率去频繁切换内核态,太亏了。于是,JDK 6 引入了极其优雅的锁升级机制:只能升级,不能降级。

    1. 无锁 -> 偏向锁(Biased Lock) “偏向第一个来的人”。如果这段时间只有线程 A 在反复进出同步块,JVM 会直接在对象的 Mark Word 里打上线程 A 的 ID(这就是 CAS 操作)。下次 A 再来,看一眼 ID 是自己,直接放行,几乎零开销。

    2. 偏向锁 -> 轻量级锁(Lightweight Lock) 如果有另外一个线程 B 来了,发现锁已经偏向了 A,偏向状态就会撤销。此时如果 A 和 B 的执行时间是错开的(没有发生真正的同时抢夺),JVM 就会升级为轻量级锁。 线程会在自己的栈帧里创建一个“锁记录(Lock Record)”,尝试用 CAS 操作把对象的 Mark Word 替换为指向自己锁记录的指针。成功了,就拿到了轻量级锁。

    3. 轻量级锁 -> 重量级锁(Heavyweight Lock,即锁膨胀) 如果在升级轻量级锁的过程中,B 发现 CAS 失败了——说明此时 A 正在里面没出来,发生了真正的竞争!轻量级锁兜不住了,必须进行“锁膨胀”。此时,JVM 才会老老实实去堆里申请刚才讲的那个 Monitor 对象,将状态彻底转为重量级锁,B 乖乖进入 EntryList 阻塞。

    🛠️ 真实业务防坑指南: 以开发苍穹外卖的并发抢单模块为例。如果在凌晨单量极少时,可能偶尔只有一个骑手访问抢单接口,此时 JVM 用的是偏向锁,性能极高。 但如果在中午用餐高峰期,上百个骑手疯狂点击抢单,并发量暴增。如果你还在核心逻辑外围加 synchronized,锁会迅速膨胀为重量级锁,导致大量处理线程 BLOCKED,上下文切换剧增,CPU 飙升但系统吞吐量断崖式下跌。 防坑策略: 在高并发场景下,直接禁用偏向锁(-XX:-UseBiasedLocking)可以减少撤销偏向锁带来的停顿(STW);同时,尽量缩小 synchronized 的包裹范围(减小临界区),或者干脆使用基于乐观锁的 CAS(如原子类)或 Redis 分布式锁来重构抢单逻辑。

    🟢 [绿灯区] 锁优化:JVM 的“微操”

    为了榨干最后一点性能,JVM 在编译阶段和运行期还会做一些优化:

    • 自旋锁: 在升级为重量级锁之前,线程获取锁失败时不会立刻挂起自己,而是原地循环(自旋)几次。这就好比等红灯时你不熄火,随时准备一脚油门踩下去,用短暂的 CPU 空转换取避免线程切换的巨大开销。
    • 锁消除: JVM 通过逃逸分析,发现你加锁的对象(比如某个方法内部的局部变量)根本不可能被其他线程访问到,就会在编译时自动把这把锁去掉。
    • 锁粗化: 如果你在一个 for 循环里疯狂对同一个对象加锁解锁,JVM 会看不下去,直接把加锁范围扩大到循环外部,只加一次。

    三、打破信息孤岛——线程间协作与通信机制

    🟡 [黄灯区] Wait/Notify 与 Sleep:带不带钥匙的区别

    为了让线程在条件不满足时歇着,Java 提供了 wait() / notify() 机制。它俩必须配合 synchronized 一起使用(也就是必须先拿到对象锁)。

    不要去背八股文里的概念,只需要记住一个通俗的比喻:

    • **sleep()**:相当于你拿着洗手间的钥匙在马桶上睡着了。你不醒,谁也进不来(不释放锁,但释放 CPU)。
    • **wait()**:相当于你发现洗手间没手纸了,主动把钥匙交出去,跑到门外的休息室(Monitor 的 WaitSet)去等(释放锁,释放 CPU)。直到别人送来了纸,并大喊一声 notify(),你才重新回去排队抢钥匙。
    🔴 [红灯区] 虚假唤醒与 while 循环检测

    如果你在代码里用了 wait(),大厂面试官一定会顺藤摸瓜,抛出这个高频的实战红灯问题:“虚假唤醒(Spurious Wakeup)听过吗?代码应该怎么写?”

    在业务场景中,我们经常会用 notifyAll() 来唤醒休息室里的所有线程。假设外卖系统里有两个线程:小哥 A 等着送“烧烤”,小哥 B 等着送“奶茶”。此时厨房产出了一份“奶茶”,并喊了一声 notifyAll()。

    反面教材(灾难现场):使用 **if** 判断

    Java

    synchronized (lock) {
    if (!hasBBQ) { // 小哥 A 醒了,但没烧烤,直接向下执行,送了个寂寞!
    lock.wait();
    }
    // 醒来后继续执行送外卖逻辑…
    }

    大厂防坑指南: 小哥 A 被误唤醒了,如果用 if,他醒来后不会再次检查条件,直接往下走,导致业务逻辑出现严重 Bug。

    标准答案:使用 **while** 循环

    Java

    synchronized (lock) {
    while (!hasBBQ) { // 醒来第一件事:再看一眼到底有没有我的外卖
    lock.wait(); // 不是我的?继续乖乖躺下睡
    }
    // 醒来后继续执行送外卖逻辑…
    }

    💡 面试官: 为什么不推荐用 notify() 而推荐 notifyAll()? 回答:notify() 只能随机唤醒 WaitSet 中的一个线程。如果厨房产出了奶茶,却用 notify() 随机唤醒了等烧烤的小哥 A,小哥 A 醒来一看不是自己的,继续 wait()。而真正等奶茶的小哥 B 还在死睡。这就导致了信号丢失,程序彻底卡死。用 notifyAll() 配合 while 循环,才是最健壮的工业级写法。

    🔴 [红灯区] LockSupport:大厂底层的“发牌官”

    wait/notify 虽然经典,但太笨重了。它必须绑定 synchronized,而且极其讲究先后顺序——如果在 wait() 之前对方就先 notify() 了,这个唤醒信号就永远丢失了,线程会死等。

    为了解决这个问题,JDK 并发包的底层(包括大名鼎鼎的 AQS 框架)都采用了一种更高级的线程原语:LockSupport.park() 和 LockSupport.unpark()。这里是很多普通开发者的知识盲区。

    它不需要加锁,可以直接挂起和唤醒指定的线程。它的底层逻辑就像是一个发牌机制:维护了一个只能存 1 或 0 的 _counter(许可)。

    核心工作流(_counter 机制):

  • **unpark(thread)**** 动作:** 给指定的线程发一张许可。_counter 变成 1。(注意:哪怕你连续 unpark 10 次,_counter 也是 1,不会累加)。
  • **park()**** 动作:** 检查自己有没有许可。如果有(_counter 为 1),就把许可消耗掉(变为 0),然后直接通行,不阻塞!如果没有许可(_counter 为 0),线程直接被挂起。
  • 🛠️ 底层原理剖析: 正是因为这个 _counter 的存在,LockSupport 完美解决了 wait/notify 顺序错乱的问题。 假设生产者速度极快,在消费者执行 park() 之前,就已经执行了 unpark(消费者)。此时消费者的 _counter 已经被置为了 1。当消费者随后执行 park() 时,发现已经有许可了,就会直接继续往下跑,而不会像 wait 那样被死死卡住。

    极简伪代码:

    Java

    Thread t1 = new Thread(() -> {
    // 即使主线程已经提前 unpark 了,这里也不会被阻塞
    LockSupport.park();
    System.out.println("收到许可,开始消费…");
    });
    t1.start();

    // 主线程可以直接精准唤醒 t1,不需要 synchronized 抢锁
    LockSupport.unpark(t1);

    四、纸上得来终觉浅——并发设计模式实战

    🟡 [黄灯区] 保护性暂停模式 (Guarded Suspension)

    一句话通俗理解: 就像你点了一份外卖,必须坐在家里等骑手送达,拿到外卖(结果)后才能接着吃饭。一个线程必须等待另一个线程产生结果才能继续往下走,这就是保护性暂停。我们平时高频使用的 Thread.join() 或者 Future.get(),底层全都是这套思想。

    这里最核心的工程素养是:绝对不能死等。 在真实的业务中,如果你调用的外部接口超时了,你的线程不能一直卡在 wait() 上,必须有一个超时撤退机制。来看最核心的防死等逻辑:

    Java

    public Object get(long timeoutMillis) {
    synchronized (lock) {
    long begin = System.currentTimeMillis();
    long timePassed = 0; // 记录已经经历的时间

    while (response == null) {
    // 计算还剩多少时间需要等
    long waitTime = timeoutMillis – timePassed;
    if (waitTime <= 0) {
    break; // 超时了,不等了,直接溜
    }
    try {
    lock.wait(waitTime); // 只等剩余的时间
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    // 醒来后(可能是被虚假唤醒),重新计算已经等了多久
    timePassed = System.currentTimeMillis() – begin;
    }
    return response;
    }
    }

    批语: 这里的精髓在于 timePassed 和 waitTime 的动态计算。因为线程如果中途被意外唤醒(虚假唤醒),再进入 wait() 时绝对不能重新等 timeoutMillis 那么久,否则超时机制就形同虚设了。

    🔴 [红灯区] 异步模式之生产者-消费者 (MessageQueue)

    这是字节跳动等头部大厂极高频的现场白板/手写代码题。

    保护性暂停是“一对一”的同步等待,但真实的高并发业务(比如外卖系统的下单与接单)往往是“多对多”的异步处理。中午高峰期大量用户疯狂下单(生产者),如果直接把请求砸给数据库或后厨派单系统(消费者),系统一秒钟就得崩。

    我们必须在中间加一个“容量有限的缓冲池”,这就是阻塞队列(MessageQueue)。生产者只管往里扔,消费者只管往外拿,完美解耦,削峰填谷。

    极简核心代码:

    Java

    class MessageQueue {
    private LinkedList<Message> list = new LinkedList<>();
    private int capacity; // 必须有容量限制!

    public MessageQueue(int capacity) {
    this.capacity = capacity;
    }

    // 消费者拿走消息
    public Message take() {
    synchronized (list) {
    while (list.isEmpty()) { // 没消息?消费者等着
    list.wait();
    }
    Message msg = list.removeFirst();
    list.notifyAll(); // 拿走一个,通知生产者可以继续放了
    return msg;
    }
    }

    // 生产者放入消息
    public void put(Message message) {
    synchronized (list) {
    while (list.size() == capacity) { // 满了?生产者等着
    list.wait();
    }
    list.addLast(message);
    list.notifyAll(); // 放进一个,通知消费者赶紧来拿
    }
    }
    }

    💡 大厂面试问:

    面试官 1:在 **take()** 和 **put()** 方法中,为什么用 **while** 判断条件,而不是 **if**?破局回答: (此处联动第三章)为了防止虚假唤醒。以 put 为例,如果队列满了,两个生产者 A 和 B 都在 wait。此时消费者拿走了一个元素并 notifyAll(),A 和 B 都醒了。A 抢到锁,塞入一个元素,队列又满了,然后 A 释放锁。如果不用 while 循环重新判断,B 接着拿到锁后会直接往下执行,强行往已经满了的队列里塞数据,直接导致越界报错。

    **面试官 2:最后的唤醒为什么用 **notifyAll()**,改成 **notify()** 会死锁吗?**破局回答: 绝对会死锁!假设队列满了,两个生产者在等。一个消费者拿走了一个数据,此时他如果只用 notify(),恰好唤醒了另一个消费者(同类唤醒同类),这个被唤醒的消费者发现队列空了,又睡过去了。此时生产者根本没收到信号,所有人都睡死过去了。必须用 notifyAll() 保证至少有一个对立阵营的线程被唤醒。

    🛠️ 真实业务防坑指南(外卖高并发场景): 在开发外卖业务的订单分发模块时,很多人为了图省事,直接用 new LinkedList<>() 或者无界队列(如默认的 LinkedBlockingQueue)做异步。一旦到了饭点,瞬间涌入几万个订单,而派单系统的消费能力每秒只有几百个,订单就会在内存队列里疯狂堆积。最终结果必然是 OOM(内存溢出),整个外卖服务宕机报错。 所以,工业级的消息队列必须设置 **capacity** 上限(如咱们代码中的 while (list.size() == capacity)),当队列满了时,宁可阻塞请求或者快速失败抛出“系统繁忙请重试”,也绝不能把内存撑爆。

    五、活跃性灾难与排查指南

    🔴_ [红灯区] 产生死锁与 Linux 线上排查定位_

    当两个或多个线程互相持有对方需要的锁,并且都在死等对方释放时,程序就彻底僵死了。这就是最致命的活跃性灾难:死锁(Deadlock)。

    不要去死记硬背教科书里那四个必要条件(互斥、不可剥夺、请求与保持、循环等待),大厂面试官和线上真实环境根本不吃这一套。他们只看两点:你怎么写出死锁的?你怎么在 Linux 黑框框里把它揪出来的?

    极简死锁代码再现:

    Java

    public void badCode() {
    Object lockA = new Object();
    Object lockB = new Object();

    new Thread(() -> {
    synchronized (lockA) {
    // 拿到 A,想要 B
    try { Thread.sleep(100); } catch (Exception e) {}
    synchronized (lockB) { System.out.println("T1 执行"); }
    }
    }, "Thread-1").start();

    new Thread(() -> {
    synchronized (lockB) {
    // 拿到 B,想要 A
    try { Thread.sleep(100); } catch (Exception e) {}
    synchronized (lockA) { System.out.println("T2 执行"); }
    }
    }, "Thread-2").start();
    }

    🛠️_ _大厂线上排查实战(排查链路):

    假设你负责的系统突然 CPU 飙高,或者部分接口一直超时转圈圈,如何在没有界面的 Linux 服务器上定位?

    第一步:找内鬼进程 (**top**)_ 输入 _top_ 命令,按大写 _P_ 根据 CPU 占用排序,或者凭业务端口找到你的 Java 进程 PID(假设是 10086)。_

    第二步:揪出作妖线程 (**top -Hp 10086**)_ 查看这个进程里到底是哪个线程在搞鬼。你会看到几个 CPU 占用极高,或者处于长时间 Block 的线程 ID(TID,假设是 10090)。此时,将这个十进制的 10090 转换为十六进制(通常用命令 _printf "%x\\n" 10090_),得到 _276a_。_

    第三步:拍下凶案现场 (**jstack 10086 > thread_dump.txt**)_ 导出当前的线程栈快照。然后去文件里搜索十六进制的 _276a_,看看它到底停在代码的哪一行! 更直接的,直接搜索 JDK 给你总结好的魔法关键字:_Found one Java-level deadlock:_。只要看到这个词,它会极其直白地告诉你:Thread-1 锁住了对象 X 正在等对象 Y,而 Thread-2 锁住了对象 Y 正在等对象 X,连代码行号都给你标得清清楚楚。_

    💡_ 真实业务防坑指南(结合苍穹外卖场景):****现象: 在开发外卖系统的骑手端时,有一个隐藏极深的 Bug。骑手 A 帮骑手 B 代送一单,系统底层发生账户资金转账。 代码逻辑如果是:_synchronized(A的账户) { synchronized(B的账户) { 执行转账 } }_。 灾难爆发: 假如某一时刻,A 给 B 转账,同时 B 也给 A 转账。线程 1 锁住了 A 等 B,线程 2 锁住了 B 等 A。一旦两个请求在同一毫秒并发,系统的这块转账业务就永久宕机了。 破局解法(锁排序): 打破循环等待条件。无论谁给谁转账,在加锁之前,先判断账户 ID 的大小。永远强制先锁 ID 小的账户,再锁 ID 大的账户。 这样所有线程获取锁的顺序就保持一致了,死锁不攻自破!_

    🟢_ [绿灯区] 活锁与饥饿:那些没那么致命的隐患_

    相比于死锁的“彻底僵死”,还有两种活跃性问题,只需了解概念即可。

    • 活锁 (Livelock):_ 线程并没有被阻塞,而是在不停地改变状态,但业务进度就是推不下去。 这就好比平时咱们喜欢捣鼓的特调咖啡,假设你要做一杯“橙 C 美式”,需要用到浓缩液和橙汁。你和那个平时有点毛躁的室友同时在厨房,你拿到了浓缩液,他拿到了橙汁。为了不卡死,你们俩都非常有礼貌地把手里的东西放下让给对方;结果下一秒,俩人又同时拿起了原先的东西。大家都在疯狂地“让步 – 尝试 – 让步”,CPU 疯狂空转,但谁也没喝上一口咖啡。 _(解法:引入随机睡眠时间,打破这种极小概率的绝对同步)
    • 饥饿 (Starvation):_ 某个倒霉的线程因为优先级太低,或者其他线程太强势(比如非公平锁里别人一直插队),导致它永远抢不到 CPU 调度,只能在角落里画圈圈。 (解法:尽量使用公平锁,或者避免人为随意调整线程的 Priority 优先级)_

    六、总结

    今天我们拆解了 Monitor 管程、剖析了线程协作(Wait/Notify 与 LockSupport 底层机制),并手写了阻塞队列与死锁排查。这带给我们的核心指导意义是:并发从来不是无脑加锁的玄学,而是严谨的资源调度艺术。 只有看透底层,你才能在遇到系统假死时迅速用 jstack 揪出内鬼,才能针对业务特点精准设计同步模式,真正做到对系统资源的敬畏与把控。

    留下悬念:拿到锁,数据就绝对安全了吗?

    通过这篇博客,我们利用锁机制解决了多线程“排队”执行的问题,完美拿下了并发三大特性中的**“原子性”**。但是,并发世界的幽灵并未就此消散:

    假设在多核 CPU 架构下,线程 A 刚刚修改了一个共享变量,线程 B 却因为 CPU 高速缓存的存在,死活读不到最新数据(可见性失效)!更可怕的是,JVM 为了极致榨干硬件性能,竟然会偷偷把你写的代码指令顺序打乱(指令重排)!

    下一篇文章**《深入理解 JMM:Volatile 关键字与 Synchronized 锁升级之路》,我们将踏入并发的另一大深水区。我将带你手撕 JMM(Java 内存模型),揭开 **volatile** 关键字背后硬核的内存屏障**原理,并带你手写大厂必考的 DCL(双重检查锁)单例模式与底层 Happens-Before 规则。我们下期见!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 「JUC」线程安全与管程机制:Synchronized 原理与等待唤醒机制
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!