Skip to content

synchronized 关键字

Java 自带的锁,用法简单,但门道很深。

先看一个问题

java
public class Counter {
    private int count = 0;

    public void increment() {
        count++;  // 两个线程同时执行,可能丢更新
    }
}

怎么让 count++ 线程安全?用 synchronized

synchronized 三种用法

用法一:修饰实例方法

锁住当前对象:

java
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;  // 同时只有一个线程能进来
    }

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

原理:锁的是 this 对象。

用法二:修饰静态方法

锁住类的 Class 对象:

java
public class StaticCounter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;  // 锁住 StaticCounter.class
    }
}

原理:锁的是 Class 对象,全局唯一。

用法三:修饰代码块

锁住指定对象,最灵活:

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

    public void increment() {
        synchronized (lock) {  // 只锁这一块代码
            count++;
        }
    }

    public void decrement() {
        synchronized (this) {  // 锁当前对象
            count--;
        }
    }

    public void reset() {
        synchronized (StaticCounter.class) {  // 锁 Class 对象
            count = 0;
        }
    }
}

synchronized 的特性

可重入:同一线程可以反复进入

java
public class ReentrantDemo {

    public synchronized void method1() {
        System.out.println("method1");
        method2();  // ✅ 可以再次获取锁
    }

    public synchronized void method2() {
        System.out.println("method2");
        method3();
    }

    public synchronized void method3() {
        System.out.println("method3");
    }
}

如果不可重入,method1 调用 method2 时就会死锁。

保证原子性

java
// ❌ 不安全
public void increment() {
    count++;  // 分三步:读→改→写,可能被打断
}

// ✅ 安全
public synchronized void increment() {
    count++;  // 完整执行完,不会被打断
}

保证可见性

java
// ✅ synchronized 保证可见性
public synchronized void write(int v) {
    value = v;  // 释放锁时,会刷新到主内存
}

public synchronized int read() {
    return value;  // 获取锁时,从主内存读取
}

synchronized 锁的是什么?

锁的是对象的「入口」,不是代码。

java
// 两个线程操作两个不同的 Counter 实例
Counter c1 = new Counter();
Counter c2 = new Counter();

c1.increment();  // 锁 c1
c2.increment();  // 锁 c2
// ✅ 互不影响,因为锁的是不同的对象
java
// 两个线程操作同一个 Counter 实例
Counter c = new Counter();

c.increment();  // 锁 c
c.decrement();  // 等 c 的锁释放
// ❌ 会互斥,因为锁的是同一个对象

synchronized 锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

JVM 对 synchronized 做了优化,不会一上来就用重量级锁:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁
  │        │           │           │
  │     一个线程    多个线程       竞争激烈
  │     反复访问    交替获取        自旋失败

偏向锁

只有一个线程访问同步块时使用。记录线程 ID,下次直接进入。

┌────────────────────────────────────────┐
│ Mark Word (64 bits)                     │
│ ┌────────────────────────────────────┐ │
│ │ 无锁状态                            │ │
│ │ 分代年龄 | 偏向锁 | 锁标志位(01)   │ │
│ └────────────────────────────────────┘ │
│                                         │
│ ┌────────────────────────────────────┐ │
│ │ 偏向锁状态                          │ │
│ │ 线程ID | Epoch | 分代年龄 | 锁标志位 │ │
│ └────────────────────────────────────┘ │
└────────────────────────────────────────┘

轻量级锁

多个线程交替访问同步块(无真正竞争),使用自旋锁。

线程 A:创建锁记录 → 把 Mark Word 复制到锁记录 → CAS 修改 Mark Word 指向锁记录
线程 B:也想获取锁,发现 Mark Word 已指向锁记录,自旋等待

重量级锁

竞争激烈时,自旋失败,升级为重量级锁。线程进入阻塞状态,不消耗 CPU。

JVM 默认延迟开启偏向锁,可通过 -XX:BiasedLockingStartupDelay=0 取消延迟。

最佳实践

1. 缩小锁的范围

java
// ❌ 不推荐:整个方法加锁
public synchronized void process() {
    prepare();              // 不需要同步
    sharedResource.modify(); // 需要同步
    cleanup();              // 不需要同步
}

// ✅ 推荐:只锁必要部分
public void process() {
    prepare();
    synchronized (this) {
        sharedResource.modify();
    }
    cleanup();
}

2. 锁对象要选好

java
// ❌ 错误:锁可变对象
private String lock = "lock";
public void method() {
    synchronized (lock) {  // 如果 lock 被改了,问题严重
        // ...
    }
}

// ✅ 推荐:锁 final 对象
private final Object lock = new Object();

3. 避免嵌套锁

java
// ❌ 嵌套过深,难维护
synchronized (lockA) {
    synchronized (lockB) {
        synchronized (lockC) {
            // ...
        }
    }
}

// ✅ 推荐:提取方法,减少嵌套
public void method() {
    synchronized (lock) {
        doRealWork();
    }
}

private synchronized void doRealWork() {
    // ...
}

4. 多线程同时访问不同对象,不需同步

java
// 两个线程分别访问不同的实例,不需要同步
Counter c1 = new Counter();
Counter c2 = new Counter();

t1: c1.increment();  // 锁 c1
t2: c2.increment();  // 锁 c2,互不影响

synchronized vs Lock

对比项synchronizedLock (ReentrantLock)
获取锁自动进入同步块手动 lock()
释放锁自动离开同步块手动 unlock()
公平锁可配置
超时等待不支持tryLock(timeout)
中断等待不支持lockInterruptibly()
条件变量Object.wait()Condition
性能JDK 6+ 已优化相当或更好

总结

  • synchronized 可以修饰方法或代码块
  • 锁的是对象,不是代码
  • 具有可重入性
  • 保证原子性、可见性、有序性
  • 锁会升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
  • 优先使用同步代码块,缩小锁的范围

基于 VitePress 构建