synchronized 是 Java 的关键字,用于修饰方法或代码块,保证同一时刻最多只有一个线程执行该段代码。其核心规则如下:
object 中的 synchronized(this) 同步代码块时,同一时刻仅有一个线程能执行,另一个必须等待。synchronized(this) 代码块时,其他线程仍可访问该对象中的非同步代码块。synchronized(this) 锁时,其他线程对该对象所有 synchronized(this) 代码块的访问将被阻塞。简单说,synchronized 为线程声明一把锁,持有锁的线程可执行同步块内指令,其他线程需等待获取锁。
作者遇到一种微妙的死锁场景:
synchronized void a() {
countDownLatch = new CountDownLatch(1);
// do something
countDownLatch.await();
}
synchronized void b() {
countDownLatch.countDown();
}
a() 由主线程调用,b() 由异步线程回调。执行结果:主线程执行 a() 后卡住,永远等待。这是典型的死锁——a 等待 b 执行,而 b 也在等待 a 释放锁。
为何 b 会等待 a?因为 synchronized 修饰实例方法时,隐式锁对象是 this(当前实例)。主线程进入 a() 时已持有 this 锁,随后调用 await() 释放 CPU 但不释放锁。异步线程回调 b() 时尝试获取同一把 this 锁,因锁被占用而阻塞,导致 countDown() 永远无法执行,主线程永久等待。
实例方法上的 synchronized 等价于 synchronized(this) { ... }。可通过以下代码验证:
public class MultiThreadSync {
public synchronized void m1() throws InterruptedException {
System.out.println("m1 call");
Thread.sleep(2000);
System.out.println("m1 call done");
}
public void m2() throws InterruptedException {
synchronized (this) {
System.out.println("m2 call");
Thread.sleep(2000);
System.out.println("m2 call done");
}
}
public static void main(String[] args) {
final MultiThreadSync obj = new MultiThreadSync();
new Thread(() -> { try { obj.m1(); } catch (Exception e) { e.printStackTrace(); } }).start();
new Thread(() -> { try { obj.m2(); } catch (Exception e) { e.printStackTrace(); } }).start();
}
}
输出:
m1 call
m1 call done
m2 call
m2 call done
m2 的同步块等待了 m1 执行完毕,证明实例方法 synchronized 的隐式锁就是实例本身。静态同步方法的锁则是该类的 Class 对象。
synchronized 除了互斥,还保证内存可见性:写线程释放锁前将工作内存中的共享变量刷新到主存,读线程获取锁后从主存读取最新值。
经典可见性问题示例:
public class NoVisibility {
private static boolean ready = false;
private static int number = 0;
private static class ReaderThread extends Thread {
@Override public void run() {
while (!ready) { Thread.yield(); }
System.out.println(number); // 可能打印 0 或无输出
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
由于指令重排序(编译器或 CPU),ready = true 可能先于 number = 42 执行,导致读线程看到 ready=true 但 number 仍为 0。或写线程未及时将 number 刷入主存,读线程一直读到旧值。
public class VisibilityFixed {
private static boolean ready = false;
private static int number = 0;
private static final Object lock = new Object();
private static class ReaderThread extends Thread {
@Override public void run() {
synchronized (lock) {
while (!ready) { Thread.yield(); }
System.out.println(number); // 必定打印 42
}
}
}
public static void main(String[] args) {
synchronized (lock) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
}
Java 内存模型(JSR-133)对 synchronized 定义了 happens-before 规则:
其他 happens-before 规则包括:单线程规则、volatile 变量规则、Thread.start() 规则、Thread.join() 规则、构造函数规则、传递性规则。
// 共享变量
Object lock = new Object();
int a = 0, b = 0, c = 0;
// 线程 A
synchronized (lock) {
a = 1; // 1
b = 2; // 2
} // 3 解锁
c = 3; // 4
// 线程 B
synchronized (lock) { // 5 加锁
System.out.println(a); // 6
System.out.println(b); // 7
System.out.println(c); // 8
}
假设线程 A 先执行:
a=1, b=2。c=3(操作 4)与操作 8 无 happens-before 关系 → 线程 B 可能打印 c=0。结论:synchronized 仅保护同步块内的变量可见性,同步块外的写入无顺序保证。