【053】Java synchronized 系列之二:锁升级的秘密,从青涩到成熟的守护之旅

在这里插入图片描述

📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta, 欢迎沟通交流

📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌


零、引入

王二的指尖在键盘上悬着,迟迟不敢按下运行键。桌上的绿萝垂下来的藤蔓扫过手背,痒痒的,却没让他分神 —— 他用 synchronized 解决了并发安全问题,但在 1000 个线程的高并发测试中,程序的响应时间从 10ms 飙到了 200ms,日志里满是 “线程阻塞” 的痕迹。

“不是说 synchronized 优化过了吗?怎么高并发下还是这么慢?” 他皱着眉,把桌上的便签纸翻得哗哗响,那上面记着哇哥上次讲的 synchronized 基础原理,却找不到解决高并发慢的答案。

哇哥端着一杯温热的蜂蜜水走过来,把杯子放在王二手边:“你以为 synchronized 只有一种样子?它就像一个慢慢长大的人,小时候(低并发)青涩又灵活,长大后(高并发)沉稳又可靠。锁升级就是它的成长之旅 —— 从偏向锁到轻量级锁,再到重量级锁,它会根据并发情况调整自己的守护方式。”

点赞 + 关注,跟着哇哥走进 synchronized 的成长之旅,把锁升级的秘密扒得明明白白,下次高并发场景用 synchronized,也能让程序跑得又稳又快。

在这里插入图片描述

一、王二的 “高并发卡顿代码”:不懂锁升级的坑

王二写的高并发计数代码,用 synchronized 修饰了核心方法,低并发时没问题,高并发下却卡顿严重。代码如下:

package cn.tcmeta.synchronizeds;


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author: laoren
 * @description: 王二的坑:高并发下用synchronized,却不知道锁升级导致卡顿
 * @version: 1.0.0
 */
public class SynchronizedHighConcurrencySample {
    private static int count = 0;
    // 高并发:1000个线程
    private static final int THREAD_COUNT = 1000;
    // 每个线程计数100次
    private static final int LOOP_COUNT = 100;


    // synchronized修饰静态方法,锁是Class对象
    private static synchronized void increment() {
        count++;
    }

    static void main() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
        CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);

        long start = System.currentTimeMillis();

        for (int i = 0; i < THREAD_COUNT; i++) {
            executorService.submit(() -> {
                try {
                    for (int j = 0; j < LOOP_COUNT; j++) {
                        increment();
                    }
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        countDownLatch.await();
        executorService.shutdown();

        long end = System.currentTimeMillis();

        System.out.println("最终计数:" + count);
        System.out.println("理论计数:" + THREAD_COUNT * LOOP_COUNT);
        System.out.println("高并发耗时:" + (end - start) + "ms");
    }

}

在这里插入图片描述
王二叹了口气:“计数是准了,但这速度也太慢了,用户肯定受不了。”

哇哥拍了拍他的肩膀,语气温柔:“这不是 synchronized 不行,是你没懂它的成长规律。低并发时,它是‘偏向锁’,像个认人的小管理员,只要是同一个线程来,直接放行,不用费劲验证;当有少量线程竞争时,它变成‘轻量级锁’,用 CAS 和线程自旋来竞争锁,不用阻塞线程;只有高并发、竞争激烈时,它才会变成‘重量级锁’,让线程阻塞等待 —— 你这 1000 个线程同时抢锁,它直接长成了最沉稳的样子,自然会慢一点,但这是最稳妥的守护方式。”

二、用 “成长之旅” 讲透锁升级:从青涩到成熟

在这里插入图片描述

哇哥拉过一把椅子坐下,从包里掏出一本旧笔记本,翻开空白页,画了一条成长曲线 —— 这是他讲解进阶技术的习惯,用成长的故事把复杂的原理讲得亲切。

“你看这条曲线,” 他指着笔记本,“synchronized 的锁升级,就像一个人的成长:

  • 婴儿期(无锁):刚创建的对象,还没有锁,就像刚出生的宝宝,还不需要承担守护责任;
  • 幼儿期(偏向锁):如果只有一个线程来访问,它就会‘偏向’这个线程,把线程 ID 存在对象头里。下次这个线程再来,直接认 ID 放行,不用任何竞争 —— 就像幼儿只认自己的爸爸妈妈,别人不来抢,就安安稳稳的;
  • 少年期(轻量级锁):有少量线程来竞争了,它不会直接让线程阻塞,而是用 CAS 指令让线程‘自旋’(循环等待),尝试获取锁 —— 就像少年遇到小争执,不会哭闹,而是试着自己解决;
  • 成年期(重量级锁):竞争太激烈了,自旋也没用,它就会调用操作系统的 mutex(互斥量),让没抢到锁的线程阻塞等待 —— 就像成年人遇到大事,会找专业的人帮忙,虽然麻烦一点,但能保证秩序。”

他顿了顿,补充道:“锁升级是不可逆的,就像人不能返老还童。一旦升级成重量级锁,就再也回不到偏向锁或轻量级锁了。”

在这里插入图片描述

👉 锁升级核心拆解(王二记在笔记本上)

王二拿出笔,跟着哇哥的话,认真地记在笔记本上,字里行间都是对技术的敬畏:

✅ 各阶段锁的核心特点

在这里插入图片描述

💯 锁升级的触发条件

  • 偏向锁→轻量级锁:有其他线程尝试获取偏向锁,且当前偏向线程不是它;
  • 轻量级锁→重量级锁:自旋次数达到阈值(JDK 默认 10 次),或者自旋的线程数超过 CPU 核心数的一半。

锁升级的底层依赖:对象头的变化

  • 无锁:对象头的 mark word(标记字)存储对象的哈希码、GC 年龄等信息;
  • 偏向锁:mark word 存储偏向线程 ID、偏向标志位(1);
  • 轻量级锁:线程创建锁记录(Lock Record),用 CAS 把 mark word 的锁记录指针指向自己;
  • 重量级锁:mark word 存储指向监视器锁(monitor)的指针,监视器锁管理阻塞线程队列。

三、代码示例:验证锁升级,优化高并发性能

在这里插入图片描述

哇哥拿过王二的鼠标,写了两个示例:一个验证锁升级的存在,一个优化高并发下的 synchronized 性能。

🔥 示例 1:验证锁升级的存在(通过打印对象头)

package cn.tcmeta.synchronizeds;

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

/**
 * @author: laoren
 * @description: 验证synchronized的锁升级(使用JOL工具安全获取对象头信息)
 * @version: 1.0.0
 */
public class LockUpgradeVerification {

    public static void main(String[] args) throws InterruptedException {
        // 初始化JOL VM信息
        VM.current().details();

        Object lock = new Object();
        System.out.println("无锁状态对象头信息:");
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());

        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("偏向锁状态对象头信息:");
                System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                try {
                    Thread.sleep(1000); // 模拟持有锁一段时间
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    System.err.println("Thread-1 被中断");
                }
            }
        }, "Thread-1");

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(500); // 让thread1先获得锁
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("Thread-2 初始化阶段被中断");
                return;
            }
            synchronized (lock) {
                System.out.println("竞争后锁状态对象头信息:");
                System.out.println(ClassLayout.parseInstance(lock).toPrintable());
            }
        }, "Thread-2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }
}


执行结果:

无锁状态对象头信息:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x001831d0
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

偏向锁状态对象头信息:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000000 (thin lock: 0x0000000000000000)
  8   4        (object header: class)    0x001831d0
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

竞争后锁状态对象头信息:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000001efc050f872 (fat lock: 0x000001efc050f872)
  8   4        (object header: class)    0x001831d0
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

👉 示例 2:优化高并发下的 synchronized 性能

package cn.tcmeta.synchronizeds;


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.LongAdder;

/**
 * @author: laoren
 * @description: 优化思路:细粒度锁+减少锁竞争,或用原子类替代
 * @version: 1.0.0
 */
public class SynchronizedHighConcurrencyOpt {

    // 优化1:细粒度锁,每个分区一个锁
    private static final int PARTITION_COUNT = 10;
    private static final int[] counts = new int[PARTITION_COUNT];
    private static final Object[] locks = new Object[PARTITION_COUNT];

    static {
        // 初始化每个分区的锁
        for (int i = 0; i < PARTITION_COUNT; i++) {
            locks[i] = new Object();
        }
    }

    // 按线程ID取模分配分区,减少锁竞争
    private static void incrementByPartition() {
        long threadId = Thread.currentThread().getId();
        int partition = (int) (threadId % PARTITION_COUNT);
        synchronized (locks[partition]) {
            counts[partition]++;
        }
    }

    static void main() throws InterruptedException {
        int THREAD_COUNT = 1000;
        int LOOP_COUNT = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
        CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);

        // 测试优化后的细粒度锁
        long start1 = System.currentTimeMillis();
        for (int i = 0; i < THREAD_COUNT; i++) {
            executorService.submit(() -> {
                try {
                    for (int j = 0; j < LOOP_COUNT; j++) {
                        incrementByPartition();
                    }
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        long end1 = System.currentTimeMillis();

        // 统计总计数
        int total = 0;
        for (int count : counts) {
            total += count;
        }
        System.out.println("细粒度锁-最终计数:" + total);
        System.out.println("细粒度锁-耗时:" + (end1 - start1) + "ms");

        // 测试用原子类替代(高并发计数最优解)
        LongAdder longAdder = new LongAdder();
        CountDownLatch countDownLatch2 = new CountDownLatch(THREAD_COUNT);
        long start2 = System.currentTimeMillis();
        for (int i = 0; i < THREAD_COUNT; i++) {
            executorService.submit(() -> {
                try {
                    for (int j = 0; j < LOOP_COUNT; j++) {
                        longAdder.increment();
                    }
                } finally {
                    countDownLatch2.countDown();
                }
            });
        }
        countDownLatch2.await();
        long end2 = System.currentTimeMillis();
        executorService.shutdown();

        System.out.println("LongAdder-最终计数:" + longAdder.sum());
        System.out.println("LongAdder-耗时:" + (end2 - start2) + "ms");
    }


}

在这里插入图片描述

王二眼睛瞪得圆圆的:“居然快了这么多!原来不是 synchronized 不行,是我用的方式不对。”

“是啊,” 哇哥笑了笑,“锁升级是 synchronized 的成长,我们用它的时候,也要根据它的成长规律来。高并发下,要么用细粒度锁减少竞争,不让它升级成重量级锁;要么用原子类替代,这都是更灵活的选择。”

四、面试必问:synchronized 进阶核心题(附答案)

在这里插入图片描述

✔️ 面试题 1:synchronized 的锁升级过程是什么?各阶段锁的适用场景是什么?

用 “成长之旅” 的例子答,面试官一听就懂:

  • 锁升级过程:无锁→偏向锁→轻量级锁→重量级锁(不可逆);
  • 各阶段适用场景:
    • 偏向锁:单线程访问,无竞争(比如单线程循环调用加锁方法);
    • 轻量级锁:少量线程竞争,自旋能获取到锁(比如 10 个线程竞争);
    • 重量级锁:高并发激烈竞争,自旋失效(比如 1000 个线程竞争)。

📢 面试题 2:偏向锁的核心作用是什么?为什么要设计偏向锁?

  • 核心作用:减少单线程访问时的锁开销;
  • 设计原因:大多数场景下,锁都是被同一个线程多次访问的,没有竞争。如果每次都用 CAS 或阻塞,会有不必要的开销;
  • 实现原理:把访问锁的线程 ID 存在对象头的 mark word 里,下次该线程访问时,直接匹配线程 ID 即可获取锁,不用任何竞争操作,几乎无开销。

🔔 面试题 3:高并发场景下,如何优化 synchronized 的性能?

  • 减小锁粒度:把大的临界区拆分成小的,用多个锁保护不同的资源(比如示例中的分区锁),减少锁竞争;
  • 避免锁升级:尽量让锁保持在偏向锁或轻量级锁状态,减少高并发竞争;
  • 用原子类替代:简单的计数 / 累加场景,用 LongAdder、AtomicInteger 等原子类替代 synchronized,无锁更高效;
  • 合理设置线程池大小:避免线程数过多导致锁竞争激烈,线程池大小建议和 CPU 核心数匹配或略多。

五、总结:synchronized 进阶核心心法(王二编的顺口溜)

王二把进阶知识点编成了顺口的小句子,贴在笔记本封面,方便随时看:

  • 锁升级不可逆,无锁偏向轻量重;
  • 单线程用偏向,少量竞争轻量中;
  • 高并发变重量,阻塞等待保秩序;
  • 优化性能有妙招,细粒度锁减竞争;
  • 简单计数原子类,无锁高效更轻松。

王二用细粒度锁优化了项目里的高并发代码,测试后响应时间从 200ms 降到了 30ms,领导看了测试报告,笑着说:“这代码写得越来越有章法了,能根据场景选对技术,才是真的懂了。”

王二拿着报告去找哇哥,脸上满是成就感。哇哥喝了一口蜂蜜水,慢悠悠说出那句收尾的话:

哇哥说:“锁的升级,就像人的成长,从青涩到成熟,都是为了更好地适应环境。synchronized 的温柔,就在于它从不会一开始就摆出强硬的姿态,而是根据实际情况调整自己的守护方式。这世上本没有一成不变的技术,只有懂得灵活应变的开发者。”

在这里插入图片描述
在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值