讨论 Java 多线程同步时,先问两件事:哪些状态被共享,以及哪些操作必须一起完成。对象锁、volatile 和原子类解决的问题有交集,但不能互相机械替换。

synchronized 锁住的是哪个监视器

写法使用的监视器
实例 synchronized 方法当前接收对象 this
synchronized(this)当前对象 this
synchronized(lock)表达式 lock 指向的对象
static synchronized 方法声明该静态同步方法的类的 Class 对象

两个不同实例的方法不因名称相同就共用一把实例锁;反过来,同一对象的两个不同同步方法会竞争同一个监视器。

独立 lock 必须是所有访问者共享的同一个对象。每次进入时 new 一个锁,或换成不同实例,就无法保护同一份状态。对象监视器的进入与释放还提供内存模型规定的可见性关系。JLS 17:线程与锁

用同一把锁保护一个完整操作

下面的类将计数修改和读取都放在同一把锁内:

public final class Counter {
    private final Object lock = new Object();
    private int count;

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public int get() {
        synchronized (lock) {
            return count;
        }
    }
}

不能只锁写入,却让需要一致性保证的读取绕开同步。如果是多个关联字段的不变量,它们也应服从同一套保护规则。

拆成不同锁并不天然提高效率;如果两个临界区共同维护一个不变量,拆锁可能先破坏正确性。是否要拆,取决于数据之间是否真的独立。

volatile 提供可见性,不提供复合操作互斥

private volatile boolean stopping;
private volatile int count;

public void requestStop() {
    stopping = true;
}

public void incrementUnsafe() {
    count++; // 仍然是读取、加一、写回三个步骤
}

对 volatile 变量的写与后续读之间存在规定的 happens-before 关系,适合表达某些简单状态发布。但 count++ 不是不可分割的整体:两个线程可能读到同一个旧值,分别加一后覆盖,丢掉一次更新。

这不等于 volatile 的单次读写都不原子。问题要具体到操作;单个计数可考虑 AtomicInteger,多个字段的一致变更则可能仍需要锁或其他并发设计。

先选择正确语义,再讨论性能

volatile 不会像监视器那样提供互斥等待,也不能因此得出它对所有任务都更快、更合适。任务是否阻塞、竞争程度和临界区大小都不同。没有相同正确性目标的比较,性能排名没有意义。

当需求是停止请求、单个数值原子更新、跨字段一致性或等待某个条件时,应分别选择机制,而不是看到“轻量级”就直接替换 synchronized。