讨论 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。