【052】Java synchronized 系列之一:原来它不是笨重的锁,是温柔的并发守护者

在这里插入图片描述


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

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

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


零、引入

王二盯着屏幕上跳动的数字,眉头拧成了疙瘩。桌上的美式咖啡已经凉透,杯壁凝的水珠顺着杯身滑下来,在桌面洇出一小片深色印记 —— 他写的用户余额修改功能,在 10 个线程同时操作时,原本该精准增减的余额,硬是从 1000 变成了 893,少了的 107 块像凭空蒸发了一样。

“怎么会这样……” 他无意识地抓着头发,指节都泛了白,“明明每个修改步骤都写对了,怎么多线程一跑就乱套?”

隔壁工位的哇哥端着一杯刚泡好的菊花茶走过来,杯口飘着几朵舒展的菊花。他瞥了眼王二的代码,又看了看他愁眉苦脸的样子,轻声笑了:“你这是让线程们‘抢着干活’了。就像一群人挤在一个小厨房里做饭,谁都抢着用锅铲、碰调料,最后锅里的菜能好吃才怪。synchronized 就是那个给厨房安门的人,让大家排队进去,一人做完一人再进。”

点赞 + 关注,跟着哇哥把 synchronized 的温柔内核扒明白,不用再怕并发乱套,下次写多线程代码,也能像在厨房从容做饭一样安稳。

在这里插入图片描述

一、王二的 “乱厨房代码”:无锁并发的陷阱

在这里插入图片描述

王二写的余额修改代码,没加任何同步措施,想当然地认为 “每个线程按步骤执行就不会错”。代码如下,透着股新手的莽撞:

package cn.tcmeta.synchronizeds;


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

/**
 * @author: laoren
 * @description: 王二的坑:无锁修改共享变量,导致并发安全问题
 * @version: 1.0.0
 */
public class UnsynchronizedBalanceSample {
    // 共享变量:用户余额
    private static int balance = 1000;
    // 线程数:10个
    private static final int THREAD_COUNT = 10;
    // 每个线程修改次数:10次(5次加10,5次减10,理论余额不变)
    private static final int LOOP_COUNT = 10;

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

        for (int i = 0; i < THREAD_COUNT; i++) {
            executorService.submit(() -> {
                try {
                    for (int j = 0; j < LOOP_COUNT; j++) {
                        if (j % 2 == 0) {
                            // 余额加10
                            balance += 1;
                        } else {
                            // 余额减10
                            balance -= 1;
                        }
                        // 模拟耗时操作
                        Thread.sleep(1);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        // 等待所有线程完成
        countDownLatch.await();
        executorService.shutdown();

        // 理论余额:1000(加10和减10抵消)
        System.out.println("最终余额:" + balance);
        System.out.println("理论余额:1000");
    }
}

在这里插入图片描述
王二叹了口气,瘫在椅子上:“我明明让每个线程交替加 1 减 1,怎么会不对呢?”

哇哥把菊花茶放在王二桌上,指尖敲了敲代码里的balance += 1:“你以为这行代码是一步完成的?其实它是‘读取当前余额→加 10→写入新余额’三步。就像你在厨房做饭,刚把菜倒进锅,还没放盐,就被另一个人把锅抢过去炒了 —— 线程切换的时候,中间状态就丢了。synchronized 的作用,就是把这三步变成‘不可分割’的一整段,就像给这三步加了个保护罩,谁都不能中途打断。”

二、用 “共享厨房” 讲透 synchronized:温柔的秩序守护者

在这里插入图片描述
哇哥拉过旁边的空椅子坐下,从口袋里掏出一张皱巴巴的便签纸,画了个简单的厨房示意图 —— 这是他的习惯,再复杂的技术,到他手里都能变成生活里的小事。

“你看这个共享厨房,” 他指着便签纸,“里面只有一个灶台(CPU)、一把锅铲(共享资源)。如果大家不排队,你抢着炒两下,我抢着翻两下,最后炒出来的肯定是糊掉的乱炖。synchronized 就像厨房门口的管理员,它手里有一把钥匙(监视器锁),只有拿到钥匙的人才能进去做饭,做完饭把钥匙还回来,下一个人再拿。”

他顿了顿,语气变得温柔:“synchronized 从来不是什么‘笨重的锁’,它是个温柔的守护者。它不会粗暴地挡住线程,只是给线程们排好队,让每个线程都能安安稳稳地完成自己的工作。它的核心就是‘互斥性’—— 同一时间,只有一个线程能进入被它保护的代码块(临界区)。”

在这里插入图片描述

📌 核心原理拆解(王二记在便签纸背面)

王二掏出笔,跟着哇哥的话,一笔一划地记在便签纸背面,字里行间都是恍然大悟:

✔️ synchronized 的核心特性

  • 互斥性:同一时间,只有一个线程能持有锁,进入临界区;
  • 可见性:线程释放锁时,会把共享变量的修改同步到主内存;线程获取锁时,会从主内存读取最新的共享变量值;
  • 原子性:保证临界区内的代码块 “不可分割”,不会被线程切换打断;
  • 可重入性:同一个线程可以多次获取同一把锁(比如递归调用加锁方法),不会死锁。

➡️ synchronized 的底层依赖:对象头与监视器锁

  • 每个 Java 对象都有一个 “对象头”,里面存储了对象的锁状态、持有锁的线程 ID 等信息;
  • synchronized 加锁时,会修改对象头的锁状态,把锁的所有者设置为当前线程;
  • 锁的本质是 “监视器锁(monitor)”,它是 JVM 层面的一个同步工具,包含等待队列和持有线程两个核心部分。

📢 synchronized 的两种加锁方式

  • 修饰方法:锁是当前对象(非静态方法)或类对象(静态方法);
  • 修饰代码块:锁是括号里指定的对象(比如synchronized (this) { … }锁的是当前对象)。

三、代码示例:用 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 SynchronizedBalanceSample {
    private static int balance = 1000;
    private static final int THREAD_COUNT = 10;
    private static final int LOOP_COUNT = 10;

    // 用synchronized修饰方法,锁是当前类对象(因为是静态方法)
    private static synchronized void updateBalance(int amount) {
        balance += amount;
    }

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

        for (int i = 0; i < THREAD_COUNT; i++) {
            executorService.submit(() -> {
                try {
                    for (int j = 0; j < LOOP_COUNT; j++) {
                        if (j % 2 == 0) {
                            updateBalance(1); // 加10
                        } else {
                            updateBalance(-1); // 减10
                        }
                        Thread.sleep(1);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

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

        System.out.println("最终余额:" + balance);
        System.out.println("理论余额:1000");
    }

}

在这里插入图片描述
王二眼睛亮了,像看到了希望:“居然真的准了!原来 synchronized 这么好用,我之前还听人说它很笨重,不敢用。”

“那是老黄历了,” 哇哥笑了笑,喝了口菊花茶,“JDK1.6 之后,synchronized 做了很多优化,比如锁升级、偏向锁、轻量级锁,早就不是以前那个笨重的重量级锁了。就像一个人,年轻时可能有些笨拙,但慢慢成长后,会变得灵活又可靠。”

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

在这里插入图片描述

📌 面试题 1:synchronized 的核心特性是什么?

用 “共享厨房” 的例子答,亲切又好懂:

  • 互斥性:同一时间只有一个线程能拿到锁,进入临界区(就像只有一个人能进厨房做饭);
  • 可见性:线程释放锁时,会把修改的共享变量同步到主内存;获取锁时,会从主内存读最新值(就像做饭的人做完后把厨房收拾干净,下个人看到的是整洁的厨房);
  • 原子性:临界区内的代码不可分割,不会被线程切换打断(就像做饭的人能完整做完一道菜,不会被中途打断);
  • 可重入性:同一个线程能多次获取同一把锁(就像做饭的人进去拿了调料,再进去拿锅铲,不用再重新排队)。

➡️ 面试题 2:synchronized 修饰方法和修饰代码块的区别是什么?锁对象分别是什么?

修饰方法:

  • 非静态方法:锁对象是当前类的实例对象(this);
  • 静态方法:锁对象是当前类的 Class 对象(比如 SynchronizedBalanceDemo.class);

修饰代码块:

  • 锁对象是括号里明确指定的对象(比如synchronized (this) { … }锁 this,synchronized (obj) { … }锁 obj);

核心区别:

  • 粒度:修饰代码块的粒度更细,只保护需要同步的代码,效率更高;修饰方法是对整个方法加锁,粒度较粗;
  • 灵活性:代码块可以指定任意锁对象,方法只能用固定的锁对象(this 或 Class 对象)。

✔️ 面试题 3:synchronized 为什么能保证可见性?

依赖 JVM 的 “内存屏障” 机制:

  • 线程获取锁时,JVM 会插入 “读屏障”,强制从主内存读取共享变量的最新值(而不是从线程本地缓存读取);
  • 线程释放锁时,JVM 会插入 “写屏障”,强制把线程本地缓存中修改后的共享变量值同步到主内存;
    这样一来,所有线程看到的共享变量都是最新的,保证了可见性。

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

在这里插入图片描述
王二把基础知识点编成了顺口的小句子,贴在显示器旁边,一眼就能看到:

  • synchronized 真温柔,并发安全靠它守;
  • 互斥可见原子性,可重入性不发愁;
  • 方法代码块都能加,锁对象要分清;
  • 非静态锁 this,静态锁是 Class;
  • 细粒度用代码块,效率更高更优秀。

王二用 synchronized 修改了项目里所有涉及共享变量的代码,测试后并发问题全解决了。领导看了测试报告,笑着拍了拍他的肩膀:“进步很快嘛,代码越来越稳了。”

王二拿着报告去找哇哥,脸上藏不住的开心。哇哥放下手里的菊花茶,慢悠悠说出那句收尾的话:

哇哥说:“技术这东西,就像巷子里的老门栓,看着朴实,却守着最实在的安稳。你之前觉得它笨重,不过是没懂它的温柔 —— 它从不是要挡住线程的脚步,而是要让每个线程都能安稳地完成自己的事。这世上本没有复杂的并发,只有没被温柔对待的代码。”

关注我,下次咱们扒一扒 synchronized 的进阶秘密 —— 锁升级的全过程,从偏向锁到重量级锁,看它如何从青涩走向成熟,成为更灵活的并发守护者!

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值