深入理解Synchronized:用法、JVM实现与锁升级机制
在多线程编程中,synchronized 是一个关键概念,也是面试中的高频考点。无论你处于哪个技术阶段,掌握它都至关重要。本文将从三个维度系统性地剖析 synchronized:使用方式、JVM 底层的运行机制,以及性能优化与锁升级过程。结合代码示例,帮助你建立更深层次的理解。
1. 使用层面的理解
synchronized 本质上是一种互斥锁,确保同一时刻只有一个线程可以执行被保护的代码块。我们可以把它想象成厕所门上唯一的钥匙,一个人拿钥匙进去后可以重复进出(锁的可重入性),但其他人在他出来之前无法进入。
根据用法不同,synchronized 锁定的目标也不同:
- 普通同步方法:锁住当前对象实例(this)。
- 静态同步方法:锁住当前类的 Class 对象。
- 同步代码块:锁住括号内指定的对象。
理解同步与异步的差异有助于把握锁的作用:
- 同步:任务按顺序交替执行,如先吃饭后看电视。
- 异步:任务同时进行,如边吃饭边看剧,提升整体效率。
以下代码示例验证了不同锁对象的影响:
public class SyncTest {
public static void main(String[] args) {
Service service = new Service();
new Thread(() -> Service.m1()).start(); // 静态同步方法
new Thread(() -> Service.m2()).start(); // 静态同步方法
new Thread(() -> service.m3()).start(); // 普通同步方法
new Thread(() -> service.m4()).start(); // 同步代码块
}
private static class Service {
public synchronized static void m1() {
System.out.println("m1 get lock");
try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("m1 release lock");
}
public synchronized static void m2() {
System.out.println("m2 get lock");
System.out.println("m2 release lock");
}
public synchronized void m3() {
System.out.println("m3 get lock");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("m3 release lock");
}
public void m4() {
synchronized (this) {
System.out.println("m4 get lock");
System.out.println("m4 release lock");
}
}
}
}
运行上述代码会发现:m1 和 m2 互斥(锁住 Class 对象),m3 和 m4 互斥(锁住同一个实例对象),但静态方法与普通方法之间不互斥,因为它们锁定的是不同的对象。
那么锁信息存储在何处?答案在对象头中。一个 Java 对象由三部分组成:对象头(Object Header)、实例数据和对齐填充。对象头中的 Mark Word 存储了哈希值、GC 信息、锁状态等。在 64 位 JVM 中,Mark Word 占 8 字节。我们可以通过 jol-core 工具来观察:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
public class ObjectMemory {
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new Fruit()).toPrintable());
}
}
class Fruit {
private boolean flag;
}
输出显示:对象头 12 字节(Mark Word + 类指针) + 实例数据 1 字节 + 对齐填充 3 字节,总共 16 字节。
2. JVM 层面的实现
以简单的同步代码块为例:
public class SyncJvmTest {
public static void main(String[] args) {
synchronized (SyncJvmTest.class) {
System.out.println("jvm sync test");
}
}
}
通过 javap -v 反编译后,可以看到 monitorenter 和 monitorexit 两条指令。JVM 规范规定:
- 每个对象都关联一个 monitor(C++ 实现,称为 ObjectMonitor)。
- 线程进入同步块时尝试获取 monitor,成功后将其计数器加 1。
- 如果线程重复进入(即可重入),计数器再次加 1。
- 其他线程竞争时,会被阻塞在等待队列中,直到持有线程将计数器置 0(释放锁)。
早期的 synchronized 属于重量级锁,因为线程阻塞/唤醒涉及操作系统用户态与内核态切换,开销大。JDK 1.6 之后引入了一系列优化,使其性能大幅提升。
3. 优化与锁升级
JDK 1.6 对 synchronized 进行了重大改进,引入了偏向锁、轻量级锁,并与重量级锁配合,形成一个逐步升级的过程。锁只能升级不能降级。各个状态的 Mark Word 标记如下:
| 锁状态 | Mark Word 标志位 | 说明 |
|---|---|---|
| 无锁 | 001 | 对象未被锁定 |
| 偏向锁 | 101 | 偏向一个线程,避免 CAS 操作 |
| 轻量级锁 | 00 | 多线程交替执行,使用 CAS 自旋 |
| 重量级锁 | 10 | 多线程竞争激烈,依赖操作系统互斥量 |
下面通过代码验证锁状态的变化,使用 jol 工具查看 Mark Word:
public class MarkWordTest {
private static Fruit fruit = new Fruit();
public static void main(String[] args) throws InterruptedException {
Task task = new Task();
Thread threadA = new Thread(task);
Thread threadB = new Thread(task);
Thread threadC = new Thread(task);
threadA.start();
threadA.join(); // 保证 A 先执行
threadB.start();
// 模拟不同场景
}
private static class Task extends Thread {
@Override
public void run() {
synchronized (fruit) {
System.out.println("Thread " + Thread.currentThread().getId() + " running");
try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(ClassLayout.parseInstance(fruit).toPrintable());
}
}
}
}
偏向锁:只启动一个线程时,Mark Word 最后三位的二进制显示为 101。偏向锁记录线程 ID,下次该线程再进入时无需任何同步操作,减少了开销。
轻量级锁:让 A 线程执行完毕后,再启动 B 线程(交替执行)。输出显示锁状态变为 00,表示升级为轻量级锁。此时线程会通过 CAS 自旋尝试获取锁,避免进入内核态阻塞。
重量级锁:启动 A、B、C 三个线程,且让它们几乎同时执行。输出显示锁状态变为 10。当多个线程同时竞争(cas 自旋超过 10 次或第三个线程加入),锁会膨胀为重量级锁,线程挂起并等待操作系统调度。
值得注意的是,重量级锁一旦升级,不可降级。JDK 1.8 中 ConcurrentHashMap 重新采用 synchronized 而非 ReentrantLock,这也体现了对 synchronized 优化成果的认可。
