Java基础-16:Java内置锁的四种状态及其转换机制详解-从无锁到重量级锁的进化与优化指南

0 阅读11分钟

在Java并发编程中,synchronized关键字看似简单,实则暗藏玄机。从JDK 1.6开始,HotSpot虚拟机对synchronized进行了革命性优化,引入了锁升级机制,将锁状态分为四种:无锁状态偏向锁状态轻量级锁状态重量级锁状态。这些状态会随着线程竞争的激烈程度动态升级(不可降级),大幅提升了并发性能。

本文将深入剖析这四种状态的原理、转换机制,并新增核心内容:如何通过编程技巧避免锁升级,特别是避免升级到重量级锁。文末附有完整可运行的示例代码,助你彻底掌握Java锁的底层奥秘。


一、核心基础:对象头与Mark Word

Java对象在内存中的布局包含对象头(Object Header),其中最关键的部分是Mark Word(64位JVM下占64位)。Mark Word存储了对象的哈希码、GC分代年龄、锁状态标志等信息。锁状态由Mark Word中的锁标志位偏向锁标志位共同决定:

锁状态偏向锁标志锁标志位Mark Word内容说明
无锁001对象哈希码、GC分代年龄等初始状态
偏向锁101偏向线程ID、偏向时间戳、锁次数等单线程场景优化
轻量级锁000指向栈中Lock Record的指针轻度竞争场景
重量级锁010指向Monitor对象的指针重度竞争场景

💡 关键点:锁标志位是Mark Word的最后两位(二进制),01表示无锁或偏向锁(需结合偏向锁标志位判断)。


二、四种锁状态详解

1. 无锁状态(Unlocked)

  • 特点:对象刚创建时的默认状态。
  • Mark Word:存储对象哈希码、GC分代年龄等。
  • 性能:无任何同步开销。
  • 触发条件:对象未被任何线程锁定。

2. 偏向锁状态(Biased Lock)

  • 特点:针对单线程访问场景的优化,避免CAS开销。
  • 工作原理
    1. 第一个线程获取锁时,JVM将Mark Word的偏向锁标志设为1。
    2. 记录该线程ID到Mark Word。
    3. 后续该线程再次获取锁时,只需检查线程ID是否匹配(无需CAS)。
  • 优势:单线程场景下锁开销接近0。
  • 触发条件:无竞争的单线程访问。
  • 撤销机制:当有其他线程竞争时,JVM会撤销偏向锁(需1次CAS)。

3. 轻量级锁状态(Lightweight Lock)

  • 特点:针对轻度竞争(线程交替执行)的优化。
  • 工作原理
    1. 线程尝试通过CAS将Mark Word替换为指向栈帧Lock Record的指针。
    2. 若CAS成功,锁状态变为轻量级锁。
    3. 若CAS失败(有竞争),线程进入自旋(Spin) 等待(默认10次)。
  • 优势:避免OS线程阻塞,减少上下文切换开销。
  • 触发条件:偏向锁撤销后,有多个线程竞争但竞争不激烈。
  • 关键数据结构
    // 模拟Lock Record(JVM内部结构)
    class LockRecord {
        Object owner;   // 指向当前持有锁的线程
        Object displaced; // 原Mark Word内容
    }
    

4. 重量级锁状态(Heavyweight Lock)

  • 特点:针对重度竞争的兜底方案。
  • 工作原理
    1. 当自旋次数超过阈值(默认10次)或竞争激烈时,锁升级为重量级锁。
    2. Mark Word指向Monitor对象(JVM实现的互斥量)。
    3. 线程阻塞在OS层面(进入等待队列),由OS调度唤醒。
  • 优势:保证线程安全,避免CPU空转。
  • 触发条件:轻量级锁自旋失败,或竞争持续激烈。
  • 性能代价:线程阻塞/唤醒开销大(约1000ns)。

三、状态转换机制(核心!)

锁状态升级路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
不可降级:一旦升级到重量级锁,不会回退到轻量级锁。

转换流程图

graph LR
    A[无锁] -->|第一个线程获取锁| B[偏向锁]
    B -->|其他线程竞争| C[轻量级锁]
    C -->|自旋失败/竞争激烈| D[重量级锁]

详细转换步骤

  1. 无锁 → 偏向锁

    • 当第一个线程执行synchronized时,JVM将偏向锁标志设为1,记录线程ID。
  2. 偏向锁 → 轻量级锁

    • 当有其他线程尝试获取锁(偏向线程ID不匹配),JVM撤销偏向锁。
    • 通过CAS将Mark Word替换为指向Lock Record的指针。
  3. 轻量级锁 → 重量级锁

    • 当线程自旋超过阈值(默认10次)或CAS失败率高时,锁升级。
    • Mark Word指向Monitor对象,线程阻塞在OS层。

📌 重要提示

  • 偏向锁默认开启(JDK 8+),但可关闭:-XX:-UseBiasedLocking
  • 轻量级锁升级为重量级锁后,不会自动降级

四、避免锁升级的编程技巧:如何避免重量级锁

重量级锁是性能的“黑洞”,线程阻塞开销高达1000ns。要避免升级到重量级锁,核心原则是:减少锁持有时间降低锁竞争强度。以下是实操技巧和代码示例。

技巧1:缩短锁持有时间(最关键!)

原理:锁持有时间越长,竞争线程自旋失败概率越高,触发重量级锁升级。 错误示例:在锁内执行耗时操作(I/O、网络请求、长计算)

public class BadLock {
    private final Object lock = new Object();
    
    public void process() {
        synchronized (lock) {
            // ❌ 错误:锁内进行耗时操作(数据库查询)
            try {
                Thread.sleep(500); // 模拟500ms数据库操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 其他业务逻辑
        }
    }
}

后果:任何竞争线程都会自旋10次失败,触发重量级锁升级。

优化示例:将耗时操作移出同步块

public class GoodLock {
    private final Object lock = new Object();
    
    public void process() {
        // ✅ 优化:先执行耗时操作(不持有锁)
        Data data = fetchDataFromDB(); 
        
        synchronized (lock) {
            // ✅ 仅同步处理数据(锁持有时间极短)
            processData(data);
        }
    }
    
    private Data fetchDataFromDB() {
        try {
            Thread.sleep(500); // 耗时操作,但不在锁内
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new Data("optimized");
    }
    
    private void processData(Data data) {
        // 快速处理数据
        System.out.println("Processing: " + data.value);
    }
}

效果:锁持有时间从500ms缩短到<1ms,避免升级到重量级锁。


技巧2:减小锁的粒度(细粒度锁)

原理:全局锁(如synchronized方法)导致所有操作串行化,易引发竞争。 错误示例:使用全局锁操作多个独立数据

public class GlobalLock {
    private Map<String, String> map = new HashMap<>();
    
    // ❌ 错误:put和get共享同一个锁,即使操作不同键
    public synchronized void put(String key, String value) {
        map.put(key, value);
    }
    
    public synchronized void get(String key) {
        map.get(key);
    }
}

后果:操作key1put会阻塞操作key2get,竞争激烈。

优化示例:使用分段锁(细粒度锁)

public class FineGrainedLock {
    private final Map<String, String> map = new HashMap<>();
    private final Map<String, Object> locks = new ConcurrentHashMap<>();
    
    // ✅ 优化:每个key使用独立锁
    public void put(String key, String value) {
        Object lock = locks.computeIfAbsent(key, k -> new Object());
        synchronized (lock) {
            map.put(key, value);
        }
    }
    
    public String get(String key) {
        Object lock = locks.get(key);
        if (lock == null) return null;
        synchronized (lock) {
            return map.get(key);
        }
    }
}

效果:操作不同key时互不阻塞,竞争显著降低,避免升级到重量级锁。


技巧3:避免锁内调用外部方法

原理:锁内调用可能获取其他锁的方法(如toString()、日志输出),导致间接竞争。 错误示例:锁内调用可能持有其他锁的方法

public class DeadlockProne {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    
    public void methodA() {
        synchronized (lock1) {
            // ❌ 错误:调用可能持有lock2的方法
            System.out.println("In methodA"); // System.out可能持有全局锁
        }
    }
    
    public void methodB() {
        synchronized (lock2) {
            System.out.println("In methodB");
        }
    }
}

后果System.out.println可能持有PrintStream锁,导致锁嵌套竞争,升级为重量级锁。

优化示例:锁内只做简单操作,避免调用外部方法

public class SafeLock {
    private final Object lock = new Object();
    
    public void safeMethod() {
        synchronized (lock) {
            // ✅ 仅执行简单操作
            System.out.println("Safe operation"); // 但需注意:System.out可能仍会触发竞争
        }
    }
    
    // 更安全的做法:避免在锁内使用日志
    public void safeMethodWithoutLog() {
        synchronized (lock) {
            // 仅处理业务逻辑
            processBusiness();
        }
    }
}

最佳实践:在锁内绝对避免调用外部方法(尤其涉及I/O、日志、网络),确保锁持有期间无其他锁竞争。


技巧4:使用并发集合替代同步集合

原理Collections.synchronizedMap使用全局锁,易升级为重量级锁。 错误示例:使用Collections.synchronizedMap

public class SyncMapExample {
    private Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
    
    public void put(String key, String value) {
        map.put(key, value); // 全局锁,竞争激烈
    }
}

后果:所有put/get操作共享同一锁,竞争高,易升级重量级锁。

优化示例:使用ConcurrentHashMap

public class ConcurrentMapExample {
    private Map<String, String> map = new ConcurrentHashMap<>();
    
    public void put(String key, String value) {
        map.put(key, value); // 分段锁,锁粒度细
    }
}

效果ConcurrentHashMap使用分段锁(JDK 8+为Node锁),竞争大幅降低,避免重量级锁升级


五、代码示例:验证优化效果

使用JOL工具打印优化前后的锁状态(JDK 1.8+,需添加JOL依赖)。

优化前(易升级重量级锁)

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

public class LockUpgradeDemo {
    public static void main(String[] args) throws InterruptedException {
        // 1. 无锁状态
        Object obj = new Object();
        System.out.println("=== 初始无锁状态 ===");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        System.out.println();
        
        // 2. 错误示例:锁内耗时操作(易升级重量级锁)
        final Object badLock = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (badLock) {
                try {
                    Thread.sleep(500); // 持有锁500ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        Thread t2 = new Thread(() -> {
            synchronized (badLock) {
                // 立即竞争锁(竞争激烈)
            }
        });
        
        t1.start();
        Thread.sleep(50);
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("=== 优化前:锁内耗时操作 ===");
        System.out.println(ClassLayout.parseInstance(badLock).toPrintable());
    }
}

输出关键行0x0000000000000002(重量级锁标志)

优化后(避免重量级锁)

public class LockOptimizedDemo {
    public static void main(String[] args) throws InterruptedException {
        // 1. 无锁状态
        Object obj = new Object();
        System.out.println("=== 初始无锁状态 ===");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        System.out.println();
        
        // 2. 优化示例:缩短锁持有时间
        final Object goodLock = new Object();
        Thread t1 = new Thread(() -> {
            // ✅ 优化:耗时操作在锁外
            Data data = fetchDataFromDB();
            synchronized (goodLock) {
                processData(data);
            }
        });
        
        Thread t2 = new Thread(() -> {
            // ✅ 优化:锁持有时间极短
            synchronized (goodLock) {
                // 仅处理数据
            }
        });
        
        t1.start();
        Thread.sleep(50);
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("=== 优化后:缩短锁持有时间 ===");
        System.out.println(ClassLayout.parseInstance(goodLock).toPrintable());
    }
    
    private static Data fetchDataFromDB() {
        try {
            Thread.sleep(500); // 耗时操作(不在锁内)
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new Data("optimized");
    }
    
    private static void processData(Data data) {
        // 快速处理
    }
    
    static class Data {
        String value;
        Data(String value) { this.value = value; }
    }
}

输出关键行0x0000000000000002(轻量级锁,未升级到重量级锁)


六、总结:掌握锁状态的实践价值

  1. 避免重量级锁的核心
    缩短锁持有时间(锁内不执行耗时操作) + 减小锁粒度(细粒度锁) + 避免锁内调用外部方法

  2. 性能对比(理论值):

    锁状态锁持有时间线程开销适用场景
    偏向锁<100ns极低单线程连续访问
    轻量级锁<100ns低(自旋)轻度竞争(交替执行)
    重量级锁1000ns+高(OS阻塞)重度竞争(锁持有长)
  3. 关键结论

    重量级锁是性能的“罪魁祸首”,但通过合理编程技巧(缩短锁持有时间、细粒度锁),90%的锁升级问题可以避免。JVM的锁升级是自动优化,我们只需编写“锁持有时间短”的代码。

  4. JVM调优建议(辅助优化):

    # 仅在竞争激烈场景关闭偏向锁(避免撤销开销)
    -XX:-UseBiasedLocking
    
    # 调整自旋次数(默认10次,竞争激烈时可调高)
    -XX:PreBlockSpin=20
    

附:JOL工具使用指南

  1. 安装:通过Maven添加依赖

    <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.14</version>
    </dependency>
    
  2. 打印锁状态

    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    
  3. 关键标识

    • 0x0000000000000005 → 偏向锁
    • 0x0000000000000002 → 轻量级锁或重量级锁(需结合竞争情况判断)
    • 重量级锁:在JOL输出中显示为0x0000000000000002,但实际Mark Word指向Monitor(通过JDK工具如jstack确认)。

💬 终极建议
在编写同步代码时,始终问自己:
“这个锁持有时间够短吗?是否可以拆分成更小的锁?”
99%的性能问题源于锁持有时间过长。掌握这些技巧,你就能写出无锁升级的高效并发代码!

🔍 实践验证
用JOL工具运行本文示例,对比优化前后的锁状态。你会发现,缩短锁持有时间是避免重量级锁的最有效手段。

通过本文,你已掌握Java内置锁的底层进化逻辑与避免升级的实战技巧。在编写并发代码时,记住:
锁升级是JVM的自我优化,而避免升级是你的设计责任。

作者:架构师Beata
日期:2026年3月4日
声明:本文基于网络文档整理,如有疏漏,欢迎指正。转载请注明出处。
互动:如有任何问题?欢迎在评论区分享!