🌌 故事背景:带魔法保护的图书馆单间
想象一个图书馆,里面有几个特殊的 "魔法单间":
-
单间规则:同一时间只能有一人进入
-
魔法标记:当有人进入时,单间外会自动挂起 "使用中" 的牌子
-
自动释放:使用者离开时,牌子会自动消失
-
排队机制:如果单间已满,其他人需要在门口排队等待
这个魔法单间就是synchronized,它是 Java 中实现同步的基础机制。
🚪 魔法单间的核心规则
1. 对象监视器(Monitor)
每个 Java 对象都有一个内置的 "监视器",就像魔法单间的 "使用中" 牌子:
- 当线程进入
synchronized块时,它会尝试获取对象的监视器 - 如果监视器已被其他线程持有,则当前线程被阻塞
- 当线程退出
synchronized块时,它会释放监视器
2. 同步块与方法
java
public class Library {
private final Object lock = new Object(); // 魔法单间的锁
// 同步方法(等同于synchronized(this))
public synchronized void readBook() {
System.out.println(Thread.currentThread().getName() + " 进入单间阅读");
try {
Thread.sleep(1000); // 模拟阅读时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 离开单间");
}
// 同步代码块
public void writeBook() {
synchronized (lock) { // 使用特定对象作为锁
System.out.println(Thread.currentThread().getName() + " 进入单间写作");
try {
Thread.sleep(1500); // 模拟写作时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 离开单间");
}
}
}
🧙 魔法背后的字节码
1. 同步方法的字节码
java
public synchronized void readBook();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 方法标志包含ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: invokestatic #5 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
13: invokevirtual #6 // Method java/lang/Thread.getName:()Ljava/lang/String;
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #8 // String 进入单间阅读
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
...
2. 同步代码块的字节码
java
public void writeBook();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #11 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter // 进入同步块,获取监视器
7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
10: new #3 // class java/lang/StringBuilder
13: dup
14: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
17: invokestatic #5 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
20: invokevirtual #6 // Method java/lang/Thread.getName:()Ljava/lang/String;
23: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
26: ldc #12 // String 进入单间写作
28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
...
55: aload_1
56: monitorexit // 正常退出同步块,释放监视器
57: goto 65
60: astore_2
61: aload_1
62: monitorexit // 异常退出同步块,释放监视器
63: aload_2
64: athrow
65: return
🧠 JVM 中的锁实现
1. 对象头(Mark Word)
每个 Java 对象在内存中都有一个对象头,其中包含锁状态信息:
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
2. 锁升级过程
java
// 简化的锁升级流程图
无锁状态 → 偏向锁 → 轻量级锁 → 重量级锁
偏向锁(Biased Locking)
- 适用于单线程环境
- 第一次获取锁时,将线程 ID 记录在对象头中
- 后续同一线程再次获取锁时,无需任何同步操作
轻量级锁(Lightweight Locking)
- 适用于多线程交替执行同步块的场景
- 通过 CAS 操作尝试获取锁
- 如果锁被其他线程持有,则升级为重量级锁
重量级锁(Heavyweight Locking)
- 适用于多线程竞争激烈的场景
- 使用操作系统的互斥量(Mutex)实现
- 线程获取锁失败会被挂起,性能开销较大
🚀 完整示例:图书馆单间使用
java
public class LibraryDemo {
private final Object readingRoom = new Object(); // 阅读单间
private final Object writingRoom = new Object(); // 写作单间
public void useReadingRoom() {
synchronized (readingRoom) {
System.out.println(Thread.currentThread().getName() + " 进入阅读单间");
try {
Thread.sleep(1000); // 模拟阅读时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 离开阅读单间");
}
}
public void useWritingRoom() {
synchronized (writingRoom) {
System.out.println(Thread.currentThread().getName() + " 进入写作单间");
try {
Thread.sleep(1500); // 模拟写作时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 离开写作单间");
}
}
public static void main(String[] args) {
LibraryDemo library = new LibraryDemo();
// 启动多个读者线程
for (int i = 1; i <= 3; i++) {
final int readerId = i;
new Thread(() -> {
library.useReadingRoom();
library.useWritingRoom();
}, "读者" + readerId).start();
}
}
}
运行结果示例
plaintext
读者1 进入阅读单间
读者1 离开阅读单间
读者1 进入写作单间
读者2 进入阅读单间
读者2 离开阅读单间
读者2 等待写作单间...
读者1 离开写作单间
读者2 进入写作单间
读者3 进入阅读单间
...
🌟 synchronized 核心特性总结
-
内置锁机制:
- 每个 Java 对象都可以作为锁
- 由 JVM 自动管理锁的获取和释放
-
锁升级优化:
- 偏向锁:单线程环境下零成本
- 轻量级锁:多线程交替执行时高效
- 重量级锁:竞争激烈时保证正确性
-
可重入性:
- 同一线程可以多次获取同一把锁
- 通过锁计数器实现
-
线程安全:
-
保证同一时间只有一个线程执行同步代码
-
解决原子性、可见性和有序性问题
-
通过这个图书馆的故事,我们理解了synchronized的核心原理:它就像图书馆的魔法单间,通过对象监视器和锁升级机制,在保证线程安全的同时,尽可能地提高性能。在实际开发中,synchronized是最简单、最常用的同步手段,适合大多数场景,而无需手动管理锁的获取和释放。