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 线程,导致性能下降。因此新项目中,如果追求极致并发,可以考虑 ReentrantLockStampedLock。但日常业务代码里,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 生成,产品信息请以官网为准。