synchronized的用法
阅读提示
建议先看题目目录,再按“概念 -> 原理 -> 场景 -> 优化”顺序复习。
每题先讲结论,再补关键机制和项目实践,回答会更稳。
本文的同步语义适用于 Java 8~25;对象布局和锁状态属于 HotSpot 实现细节。下文将 JDK 8 的传统锁模型与 JDK 15、23、25 的变化分开说明,不把历史升级路径当作所有 JVM 的规范。
1、synchronized的用法
1. 修饰实例方法
当synchronized用于修饰一个实例方法时,该方法被称为同步方法。这意味着在同一时刻,只有一个线程可以持有同一实例的监视器并执行受它保护的代码。这种同步是基于对象的实例锁,即每个对象都有一个锁,当线程进入一个对象的同步方法时,它必须先获得该对象的锁。如果另一个线程已经持有该对象的锁,则该线程必须等待,直到锁被释放。
public synchronized void syncMethod() {
// method body
}2. 修饰静态方法
修饰静态方法时,同步是基于类对象(Class对象)的锁。这意味着所有实例共享同一把锁。当一个线程进入一个静态的同步方法时,它获得的是该类的锁。如果其他线程已经持有该类的锁,则新来的线程必须等待。
public static synchronized void staticSyncMethod() {
// method body
}3. 修饰代码块
当synchronized用于修饰一个代码块时,它允许你指定一个锁对象。这种方式提供了更细粒度的控制,因为它允许你只同步访问特定资源和数据部分的代码。代码块中的代码在执行时必须持有指定的锁对象。
public class Counter {
private final Object lock = new Object();
private int count;
public void increment() {
synchronized (lock) {
count++;
}
}
public int get() {
synchronized (lock) {
return count;
}
}
}同一 Counter 实例的所有调用共享 lock。若在方法内部每次 new Object() 再加锁,不同调用锁住不同对象,无法保护共享计数。同理,不同实例的同步方法可以并发;实例锁 this 与静态方法锁住的声明类 Class 对象也不是同一把锁。同名类由不同类加载器定义时,其 Class 对象不同。
机制上,synchronized 同时提供互斥与可见性:线程对监视器的解锁 happens-before 后续对同一监视器的加锁,因此上例的写入可被随后持锁读取的线程观察到。所有共享读写都应遵守同一锁协议,单独给写操作加锁不足以保证未同步读取的可见性。锁可重入;正常返回或抛出异常离开同步区域都会释放对应的锁,但不会回滚已完成的数据修改。
线程 A:加锁 lock → count++ → 解锁 lock
↓ happens-before
线程 B: 加锁 lock → 读取 count → 解锁 lock这些保证来自 JLS §17.1 与 §17.4.5,与具体采用哪种锁优化无关。
2、锁升级
(1)、内存布局 (对象的内存布局)

在 Java 中,每个对象的内存布局是由 Java 虚拟机(JVM)的具体实现决定的,例如 Oracle HotSpot JVM。这里,我们可以讨论 HotSpot JVM 中普通(非数组)对象的典型内存布局。对象的内存布局通常包括三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
1. 对象头(Object Header)
以 64 位 HotSpot、传统对象头(未启用紧凑对象头)为例,对象头包括标记字(Mark Word)和类型指针。JDK 25 的紧凑对象头是可选的产品特性,启用后可将对象头缩减为 8 字节,不能沿用这里的两段大小相加;参见 JDK 25 发布说明。
- Mark Word:这部分通常占用 8 字节,用于存储对象自身的运行时数据,如身份哈希码、锁状态、GC 年龄等;不同状态复用这些位,并非同时存放全部信息,也不是通用的对象存活标记。
- Class Pointer:启用压缩类指针时通常占用 4 字节,否则通常为 8 字节;类指针压缩与普通对象引用压缩是不同的配置
2. 实例数据(Instance Data)
实例数据部分占用的字节数取决于对象的具体字段。以下是传统 HotSpot 中常见的字段存储大小,并非 Java 规范对对象物理布局的保证(尤其是 boolean):
boolean和byte:1 字节char和short:2 字节int和float:4 字节long和double:8 字节- 引用类型(如对象引用):在 64 位 JVM 中通常是 4 字节(使用压缩指针时)或 8 字节。
字段的排列可能会由 JVM 进行优化,以减少由于内存对齐引起的空间浪费。
3. 对齐填充(Padding)
HotSpot 的对象对齐通常为 8 字节,可由 -XX:ObjectAlignmentInBytes 配置。JVM 可能在字段之间或对象末尾添加填充,以满足字段布局与对象对齐要求;8 字节并不是计算机内存寻址的最小单位。
示例
考虑一个简单的 Java 对象:
public class Example {
int a; // 4 字节
boolean b; // 1 字节
}在这个例子中,对象的内存布局可能如下:
- 对象头:12 字节(压缩类指针)或 16 字节(非压缩)
- 实例数据:
int4 字节 +boolean1 字节 = 5 字节 - 对齐填充:按 8 字节对齐时,12 字节头加 5 字节字段共 17 字节,需补齐 7 字节;16 字节头加 5 字节字段共 21 字节,需补齐 3 字节。
因此在上述简化布局下,两者都是 24 字节(12+5+7 或 16+5+3)。实际偏移与大小仍取决于 JVM 版本、字段布局和对齐配置;这不是本文实测结果。
Java 示例:简单的 Person 类
考虑下面的 Java 类 Person,它有两个字段:name 和 age。此示例将帮助我们理解类型指针在不涉及继承的情况下如何工作。
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void printInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
public static void main(String[] args) {
Person person = new Person("John Doe", 30);
person.printInfo(); // 输出: Name: John Doe, Age: 30
}
}类型指针的作用
当我们创建 Person 类的一个实例时:
- 对象创建:实例具有字段存储空间、对象头和可能的填充;JIT 也可能通过逃逸分析消除实际分配。
- 类型指针:在传统 HotSpot 布局中,它关联到
Person的类元数据,用于描述字段布局和类型等信息。 - 方法调用:虚方法分派可以利用类元数据,但不能认为每次调用都必须沿类型指针查表;JIT 可以去虚拟化甚至内联
printInfo()。类型指针也不提供任意“热部署后总能找到新代码”的保证。
(2)、markword 里面具体存的是什么东西

3、synchronized早期为什么叫做重量级锁
“重量级”主要指依赖监视器、在竞争下可能阻塞和唤醒线程的较高开销。对于平台线程,真正挂起与唤醒可能涉及操作系统调度、用户态与内核态切换,以及线程上下文切换。
这不等于每次进入 synchronized 都要向操作系统申请互斥量。HotSpot 可以通过用户态快速路径获取锁,膨胀后的监视器也可以先尝试 CAS 或自旋,只有需要阻塞时才走相应等待路径。监视器膨胀、系统调用、线程上下文切换是不同事件,不能简单画等号。
4、锁升级过程
JDK 6 时代的 HotSpot 优化包括偏向锁、轻量级锁和自适应自旋等。下面的状态讲解以 JDK 8 传统 HotSpot、开启偏向锁 为历史背景,位布局可对照仍保留此模型的 OpenJDK 17 markWord.hpp。这些状态不是 Java 语言规范要求的固定实现。
无锁状态
- 锁状态:当前未持有锁。传统布局的低两位为
01,还需结合偏向位区分普通未锁定头与可偏向头,不能仅凭01判断能否偏向。
偏向锁
偏向锁优化同一线程反复进入的场景。偏向头包含 JavaThread* 等信息,并不是 Java 层 Thread.getId() 的值;低两位为 01,偏向位为 1。偏向关系可以在退出同步块后保留,不能据此断定线程仍在临界区。
其他线程进入时可能触发重偏向或撤销,再按普通路径加锁;不是一出现第二个线程就必然一路膨胀。JEP 374 从 JDK 15 起默认禁用并废弃偏向锁,所以不能将其作为现代 JDK 的默认必经阶段。
轻量级锁
传统栈锁路径会建立锁记录,保存原对象头,并尝试用 CAS 将对象头替换为指向栈中锁记录的指针,低两位为 00;失败还需区分重入与竞争等情形。
重量级锁
竞争等情况可能触发监视器膨胀。传统布局低两位为 10,关联 ObjectMonitor,用来维护所有者及竞争、等待等状态。获得监视器失败的线程可能先自旋,再进入等待路径;不是所有竞争线程都会立即由操作系统挂起。wait() 也可能触发膨胀,并会释放调用对象的监视器,在返回前重新获得它。
补充内容:
- 膨胀与自旋不能归纳成“自旋固定 10 次后升级”。自旋策略取决于具体 HotSpot 版本及运行状态,旧版
-XX:PreBlockSpin不应当作现代 JDK 的通用调优选项。 - 这不是一条每个对象必经的单向流水线:可以跳过偏向阶段;空闲监视器也可能被 JVM 回收(deflation),不宜概括为“锁永远只能升级”。
- JDK 23 默认改用新的 Lightweight Locking,见 官方发布说明的 LockingMode 变更。其快速路径不再把传统栈锁记录指针放入对象头,不能照搬上面的
00指针解释。JDK 25 若启用紧凑对象头,布局还会变化。
synchronized在java代码层面表示
package com.javatiaozao;
public class Main {
public static void main(String[] args) {
Object o = new Object();
synchronized (o) {
System.out.println("我已经上锁了");
}
System.out.println("Hello world!");
}
}synchronized在JVM汇编层面表示
这里展示的是 JVM 字节码,不是 CPU 机器汇编。同步代码块通常编译为 monitorenter / monitorexit,异常路径也要解锁;下列摘录省略了异常表,偏移和常量池编号仅作示意。同步方法则使用 ACC_SYNCHRONIZED 标志,由 JVM 在方法调用和退出时处理监视器,不要求方法体包含这两条指令。参见 JVMS §3.14。
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init> : ()V>
7 astore_1
8 aload_1
9 dup
10 astore_2
11 monitorenter
12 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
15 ldc #4 <我已经上锁了>
17 invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>
20 aload_2
21 monitorexit
22 goto 30 (+8)
25 astore_3
26 aload_2
27 monitorexit
28 aload_3
29 athrow
30 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
33 ldc #6 <Hello world!>
35 invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>
38 return