文章目录

📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌
📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕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 的进阶秘密 —— 锁升级的全过程,从偏向锁到重量级锁,看它如何从青涩走向成熟,成为更灵活的并发守护者!


905

被折叠的 条评论
为什么被折叠?



