synchronized在Java中除了注释,修饰方法有什么作用
该专题还在整理中。
很多人调侃 synchronized 是“Java 第一注释关键字”,因为它除了让代码看起来像“线程安全”之外,好像啥也没干。但实际上,synchronized 修饰方法是 Java 内置锁(Monitor Lock)最核心的语法糖,它保证了多线程环境下方法内部状态的原子性、可见性和有序性——这绝不是注释,而是实打实的并发安全屏障。下面我拆开揉碎了讲清楚它到底干了什么,以及为什么你感觉它“没用”往往是因为用错了场景。
一、synchronized 修饰方法到底锁住了什么?
很多初级开发者以为 synchronized 方法是锁住了“方法代码本身”,这是误解。它锁住的是调用该方法的对象实例(this),或者如果是静态方法,锁住的是该类的 Class 对象。换句话说,它本质上等同于在方法内部用 synchronized(this) { ... } 或 synchronized(Class.class) { ... } 包裹了全部方法体。
这意味着:
- 同一个对象实例上,两个线程不能同时进入该对象的任意一个 synchronized 实例方法(包括不同方法,因为锁的是同一个 this)。
- 不同对象实例上的 synchronized 方法互不干扰,因为它们持有不同的锁。
- 静态 synchronized 方法锁的是类级别,与实例方法锁不互斥——这是一个常被忽视的坑。
二、除了“注释”,它实打实提供的三大保障
咱们抛开理论,直接看 JVM 规范与内存模型(JMM)层面,synchronized 修饰方法会强制做三件事:
1. 原子性:方法体变成不可分割的临界区
一旦线程进入 synchronized 方法,在执行完方法体(包括异常路径,会自动释放锁)之前,其他试图进入该对象任何 synchronized 方法的线程都会被阻塞(BLOCKED 状态)。这保证了像 count++ 这种非原子操作在方法内不会被打断。注意:它不能阻止外部代码不通过该对象锁直接修改共享变量,所以“锁的是对象,保护的是资源”这句话要记牢。
2. 可见性:解锁前的修改,必然对后续加锁线程可见
这是最容易被忽略但极其关键的一点。JMM 规定:线程释放锁时,会把工作内存中的共享变量刷新到主内存;线程获取锁时,会强制使工作内存中的该变量失效,重新从主内存读取。所以 synchronized 方法之间传递数据是安全的,不会出现“我改了你看不到”的脏读问题。
3. 有序性:禁止指令重排的穿透
编译器和 CPU 为了优化会重排指令,但 synchronized 块的加锁与解锁有内存屏障效应,阻止了重排越过临界区边界。这保证了方法内的代码在逻辑上看起来是“顺序执行”的,不会出现“先赋值后检查标志位”这种颠倒导致的诡异问题。
| 保障 | 底层机制 | 典型失效场景(如果不用 synchronized) |
|---|---|---|
| 原子性 | Monitor Enter/Exit 互斥 | 多线程同时 i++,结果小于预期 |
| 可见性 | 锁释放刷新主内存,锁获取失效缓存 | 一个线程改了 flag,另一个线程死循环看不到 |
| 有序性 | 内存屏障禁止重排 | 双重检查锁(DCL)中未加 volatile 的实例引用 |
三、修饰方法 vs. synchronized 代码块:该怎么选?
既然方法锁等价于 synchronized(this) 包住全部方法体,那问题来了:如果方法里只有 1% 的代码需要保护,剩下 99% 是耗时操作,你锁整个方法就是灾难。这会让并发性能断崖式下降。所以实际工程中,我个人的原则是:
- 方法体很短(比如 getter/setter、简单状态判断),且整个方法都需要保护,用修饰方法简洁清晰。
- 方法体很长,或者里面有 IO、网络调用、sleep 等阻塞操作,务必缩小锁范围,用
synchronized(lockObject) { ... }包住临界区。 - 如果你需要锁不同的资源,或者用多个不同锁来降低竞争,必须用代码块。
举个例子,一个缓存类:
public class Cache {
private final Object lock = new Object();
private Map<String, Object> map = new HashMap<>();
// 方法整体很短,直接用 synchronized 修饰没问题
public synchronized Object get(String key) {
return map.get(key);
}
// 方法里有耗时逻辑,锁整个方法会阻塞其他读操作
public void putWithExpensiveOp(String key, Object val) {
// 模拟耗时计算
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock) {
map.put(key, val);
}
}
}
上面第二个方法如果写成 public synchronized void putWithExpensiveOp(...),那么所有读操作都会被这个 sleep 阻塞——这是典型的“注释式同步”反模式。
四、修饰静态方法:类锁的独特行为
静态 synchronized 方法锁的是 Class 对象,与实例锁是两套锁。这意味着:
- 一个线程可以同时执行“一个实例的 synchronized 方法”和“该类的静态 synchronized 方法”,因为锁不同。
- 所有实例共享类锁,所以静态同步方法全局唯一,适合控制类级别状态(如全局计数器、连接池管理)。
这里有个经典面试陷阱:如果子类 override 了父类的 synchronized 方法,并且没加 synchronized 关键字,那这个方法就不是同步的。因为 synchronized 不是方法签名的一部分,它不会被继承。子类必须显式声明。
五、为什么你感觉它“没用”?—— 三个常见误用场景
我见过太多人说“synchronized 没用”,排查后发现全是误用:
场景一:锁的对象不是共享的
比如每次 new 一个 Runnable 传给线程,Runnable 里的 synchronized 方法锁的是各自 new 出来的不同对象,那自然互不阻塞。正确做法是让多个线程共享同一个 Runnable 实例,或者用静态锁。
场景二:锁了写操作,但读操作没锁
比如写方法加了 synchronized,读方法没加,那读线程依然可能读到中间状态。可见性保障只在“锁的配对”上生效——读和写必须用同一个锁。
场景三:在方法内调用了外部方法,导致死锁或锁失效
比如 synchronized 方法里又去调另一个对象的 synchronized 方法,如果锁顺序不一致,就会死锁。另外,如果方法内抛出异常且未捕获,锁会自动释放,但状态可能已部分修改——所以最好在 finally 里做状态回滚或使用 try-finally 包裹逻辑。
六、现代 Java 中的演进:synchronized 还是不是首选?
Java 6 以后,synchronized 引入了偏向锁、轻量级锁、重量级锁的升级路径,性能已大幅提升。但到了 Java 21+,虚拟线程(Project Loom)普及后,synchronized 在虚拟线程中如果阻塞在锁上,会钉住 Carrier 线程,导致性能下降。因此新项目中,如果追求极致并发,可以考虑 ReentrantLock 或 StampedLock。但日常业务代码里,synchronized 的可读性和自动释放锁的便利性,依然是首选。
简单对比一下:
| 特性 | synchronized 方法 | ReentrantLock |
|---|---|---|
| 锁释放 | 自动(异常也释放) | 必须 finally 中 unlock |
| 可中断 | 否(不可中断等待) | 是(lockInterruptibly) |
| 公平性 | 非公平(默认) | 可指定公平/非公平 |
| 条件变量 | 使用 wait/notify,较原始 | 支持多个 Condition 队列 |
| 虚拟线程友好度 | 阻塞会钉住 carrier | 同样存在问题,但 ReentrantLock 在 Loom 中可被优化 |
七、总结:synchronized 修饰方法到底有什么用?
一句话:它是 Java 语言层面对“临界区”的最简单声明,让一个方法体变成原子操作,同时提供跨线程的内存可见性和有序性保证,且不写一行显式加锁/解锁代码。它不是注释,而是 JVM 通过字节码层面的 monitorenter/monitorexit 指令强制执行的互斥协议。它最大的价值在于“简单且正确”,而最大的陷阱在于“容易锁错对象或锁范围过大”。
如果你发现自己写的 synchronized 方法看起来像注释,先检查锁对象是否唯一、读写是否配对、范围是否过大。把这三点想清楚,它就是你并发工具箱里最顺手的那把螺丝刀。
相关问题延伸
1. volatile 与 synchronized 修饰方法在可见性上有什么区别?
volatile 只保证可见性和有序性,不保证原子性;synchronized 三者都保证。但 volatile 没有锁竞争,性能更好,适合写单读多的标志位场景。
2. 为什么双重检查锁(DCL)中除了 synchronized 还要加 volatile?
因为 synchronized 只能保证锁内代码的有序性,但对象创建过程(分配内存、初始化、引用赋值)可能被重排,导致另一个线程拿到未初始化完成的对象。volatile 禁止了该重排。
3. synchronized 方法在异常抛出时锁会自动释放吗?
会。JVM 保证无论方法正常结束还是异常结束,都会执行 monitorexit 释放锁。但要注意,异常可能使共享变量处于不一致状态,需要业务代码自行处理。
4. 同步方法被继承时,子类方法是否自动同步?
不会。synchronized 不属于方法签名,子类 override 时必须显式加上 synchronized 关键字,否则就是非同步的。
5. 在 Java 21 虚拟线程下,synchronized 方法还有必要用吗?
如果方法内没有阻塞操作,synchronized 依然没问题。但如果方法内包含 IO 或 sleep 等阻塞,synchronized 会钉住 carrier 线程,建议改用 ReentrantLock 或 JUC 的原子类来避免。日常业务中,保持简单优先原则即可。
内容由 AI 生成,产品信息请以官网为准。












