锁消除(Lock Elision)与锁粗化

理解这些优化,是理解 Java 并发性能的关键。

锁消除(Lock Elision)

原理

锁消除基于逃逸分析。如果 JIT 编译器通过逃逸分析判断一个对象的锁不可能被其他线程获取,就会消除这个锁。

// 锁消除前
public void process() {
    Object lock = new Object();  // 只在方法内使用
    synchronized (lock) {
        count++;
    }
}

// 锁消除后
public void process() {
    Object lock = new Object();
    count++;  // synchronized 被消除
}

触发条件

锁消除需要满足以下条件:

flowchart TD
    A["synchronized 代码"] --> B{"锁对象是否逃逸?"}
    B -->|"否"| C["锁是否只被当前线程持有?"}
    C -->|"是"| D["锁消除"]
    C -->|"否"| E["保留锁"]
    B -->|"是"| E

示例

// 示例 1:局部对象锁
public void append(StringBuilder sb) {
    synchronized (sb) {  // sb 逃逸?不逃逸
        sb.append("text");
    }
}

// JIT 可能消除锁,因为 sb 只在方法内使用

// 示例 2:逃逸对象锁
public void store(Object obj) {
    synchronized (obj) {  // obj 可能逃逸
        cache.add(obj);
    }
}
// JIT 不会消除锁,因为 obj 可能被其他线程访问

锁粗化(Lock Coarsening)

原理

锁粗化将多个相邻的 synchronized 块合并为一个,减少锁的获取和释放次数。

// 锁粗化前
public void process() {
    synchronized (lock) {
        doSomething();
    }
    // 其他代码
    synchronized (lock) {
        doOther();
    }
}

// 锁粗化后
public void process() {
    synchronized (lock) {
        doSomething();
        // 其他代码
        doOther();
    }
}

触发条件

锁粗化发生在以下情况:

条件说明
相邻同步块使用同一个锁
中间代码简单中间代码不释放锁
性能收益合并后收益大于开销

示例

// 锁粗化前
public void update(Point p) {
    synchronized (this) {
        p.x = 1;
    }
    synchronized (this) {  // 可以合并
        p.y = 2;
    }
}

// 锁粗化后
public void update(Point p) {
    synchronized (this) {
        p.x = 1;
        p.y = 2;  // 合并
    }
}

轻量级锁

原理

轻量级锁是另一种锁优化,它使用 CAS 操作避免线程阻塞:

flowchart TB
    subgraph 轻量级锁获取
        A["进入同步块"] --> B["Mark Word\n是否偏向?"]
        B -->|"无锁"| C["CAS 尝试获取锁"]
        C -->|"成功"| D["获取轻量级锁"]
        C -->|"失败"| E["膨胀为重量级锁"]
    end

锁升级

flowchart LR
    Unlocked["无锁"] -->|"偏向"| Biased["偏向锁"]
    Biased -->|"竞争"| Light["轻量级锁"]
    Light -->|"竞争"| Heavy["重量级锁"]
    
    Unlocked -->|"CAS 失败"| Light
    
    style Heavy fill:#ff6b6b

偏向锁

偏向锁将锁偏向第一个获取它的线程:

// 偏向锁原理
public class BiasedLock {
    synchronized void process() {
        // 第一次获取偏向锁
        // 后续进入只需要检查是否是同一个线程
    }
}

锁优化参数

偏向锁参数

# 启用偏向锁(默认)
java -XX:+UseBiasedLocking

# 禁用偏向锁
java -XX:-UseBiasedLocking

# 偏向延迟
java -XX:BiasedLockingStartupDelay=0

锁消除参数

# 启用锁消除(默认)
java -XX:+DoEscapeAnalysis

# 打印锁消除信息
java -XX:+PrintEliminateLocks

观察锁优化

打印锁消除信息

# 打印锁消除信息
java -XX:+PrintEliminateLocks \
     -XX:+UnlockDiagnosticVMOptions \
     -jar application.jar

# 输出示例
Eliminated locks: [0,1,0]  // 消除的锁数量

打印偏向锁信息

# 打印偏向锁信息
java -XX:+PrintBiasedLockingStatistics \
     -XX:+UnlockDiagnosticVMOptions \
     -jar application.jar

锁优化对性能的影响

性能测试

// 锁优化性能对比
public class LockOptimizationTest {
    private int counter = 0;
    
    // 无锁
    public void noLock() {
        counter++;  // 模拟无锁
    }
    
    // 消除锁后
    public void eliminatedLock() {
        // JIT 消除 synchronized
        synchronized (this) {
            counter++;
        }
    }
}

性能提升

优化类型性能提升说明
偏向锁10%~30%单线程无竞争
轻量级锁0%~10%少量竞争
锁消除5%~15%对象不逃逸
锁粗化3%~10%减少锁操作

最佳实践

1. 缩小同步范围

// 不推荐
public void process() {
    synchronized (this) {
        // 大量不相关的代码
        doA();
        doB();
        doC();
    }
}

// 推荐
public void process() {
    doA();
    synchronized (this) {
        counter++;
    }
    doB();
    doC();
}

2. 减少锁持有时间

// 不推荐
public void process() {
    synchronized (this) {
        prepare();  // 不需要同步的操作
        calculate();  // 需要同步
        format();  // 不需要同步的操作
    }
}

// 推荐
public void process() {
    prepare();
    synchronized (this) {
        calculate();
    }
    format();
}

3. 使用局部变量

// 有利于锁消除
public void process() {
    Object lock = new Object();  // 局部对象
    synchronized (lock) {
        // JIT 可能消除锁
    }
}

// 不利于锁消除
public void process() {
    synchronized (this) {  // this 可能逃逸
        // 锁无法消除
    }
}

4. 考虑无锁数据结构

// 无锁计数器
public class LockFreeCounter {
    private AtomicInteger counter = new AtomicInteger();
    
    public void increment() {
        counter.incrementAndGet();  // CAS 操作
    }
}

注意事项

锁升级不可逆

一旦偏向锁或轻量级锁升级为重量级锁,就无法回退。

高竞争场景

在高竞争场景下,锁消除可能适得其反:

// 高竞争场景
public void process() {
    synchronized (lock) {  // 高竞争
        counter++;
    }
}

在这种情况下,使用 java.util.concurrent.atomic 或无锁数据结构可能更好。