Java 锁事详解

文档说明

✅ 严格区分:锁(Lock) vs 同步器(Synchronizer) vs 原子操作

✅ 覆盖 JDK 17 所有锁相关机制(含 JVM 底层优化)

✅ 每类包含:原理深度解析 + 规范代码示例 + 适用场景 + 注意事项

✅ 标注 JDK 17 特性变更(如偏向锁废弃)

✅ 代码经 JDK 17 编译验证,含最佳实践注释

如果不对欢迎指正!最近在复习精进这部分所以写的这个文章


一、锁的完整分类体系


二、核心锁机制详解

1️⃣ synchronized(内置监视器锁)

🔬 原理深度
  • Monitor 机制:每个 Java 对象头(Mark Word)关联 Monitor

  • 锁升级路径

    (JDK 6+ 优化):

    复制代码
    无锁 → 偏向锁 → 轻量级锁(自旋) → 重量级锁(OS Mutex)
  • JDK 17 关键变更:

    • 偏向锁 默认禁用-XX:-UseBiasedLocking
    • 启用需显式参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
    • 原因:现代应用多线程竞争频繁,偏向锁撤销成本高
💻 规范示例
java 复制代码
public class SynchronizedDeepExample {
    private int count = 0;
    private static final Object LOCK = new Object(); // 推荐:专用锁对象

    // 实例方法锁(锁 this)
    public synchronized void increment() {
        count++;
    }

    // 静态方法锁(锁 Class 对象)
    public static synchronized void staticTask() {
        System.out.println("Static synchronized");
    }

    // 代码块锁(推荐:细粒度控制)
    public void safeUpdate(int value) {
        synchronized (LOCK) { // 避免锁 this 或 String 常量
            count = value;
        }
    }

    // 死锁预防示例:按固定顺序获取锁
    public void transfer(SynchronizedDeepExample target, int amount) {
        SynchronizedDeepExample first = this.hashCode() < target.hashCode() ? this : target;
        SynchronizedDeepExample second = (first == this) ? target : this;
        
        synchronized (first) {
            synchronized (second) {
                this.count -= amount;
                target.count += amount;
            }
        }
    }
}
⚠️ 关键注意事项
问题 解决方案
锁粗化 JVM 自动优化,避免频繁加解锁
锁消除 JIT 逃逸分析,无竞争时消除锁
死锁风险 使用 jstack 检测,按固定顺序加锁
锁对象变更 严禁在同步块内修改锁引用

2️⃣ ReentrantLock(显式可重入锁)

🔬 原理深度
  • AQS(AbstractQueuedSynchronizer)

    核心:

    • CLH 队列变体管理等待线程
    • state 字段记录锁持有次数(可重入计数)
    • Node 节点含 waitStatus(CANCELLED, SIGNAL 等)
  • 公平性:

    • 非公平锁(默认):新线程尝试插队获取锁
    • 公平锁:严格 FIFO,避免线程饥饿(吞吐量略低)
💻 规范示例
java 复制代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockCompleteExample {
    private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();
    private int buffer = 0;
    private static final int CAPACITY = 10;

    // 可中断锁获取
    public void interruptibleTask() throws InterruptedException {
        lock.lockInterruptibly(); // 响应中断
        try {
            // 临界区操作
        } finally {
            lock.unlock();
        }
    }

    // 超时尝试锁
    public boolean tryLockWithTimeout() throws InterruptedException {
        if (lock.tryLock(2, TimeUnit.SECONDS)) {
            try {
                // 执行任务
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false; // 超时未获取
    }

    // 生产者-消费者(Condition 应用)
    public void produce() throws InterruptedException {
        lock.lock();
        try {
            while (buffer >= CAPACITY) {
                notFull.await(); // 释放锁并等待
            }
            buffer++;
            System.out.println("Produced: " + buffer);
            notEmpty.signal(); // 唤醒消费者
        } finally {
            lock.unlock();
        }
    }

    public void consume() throws InterruptedException {
        lock.lock();
        try {
            while (buffer <= 0) {
                notEmpty.await();
            }
            buffer--;
            System.out.println("Consumed: " + buffer);
            notFull.signal();
        } finally {
            lock.unlock();
        }
    }

    // 查询锁状态(调试/监控)
    public void printLockInfo() {
        System.out.println("Locked: " + lock.isLocked());
        System.out.println("Held by current: " + lock.isHeldByCurrentThread());
        System.out.println("Hold count: " + lock.getHoldCount());
        System.out.println("Queue length: " + lock.getQueueLength());
    }
}
✅ 优势对比 synchronized
特性 ReentrantLock synchronized
可中断
超时获取
公平策略
多条件队列 ✅ (Condition) ❌ (仅1个wait set)
锁状态查询
JVM 优化 有限 深度优化

3️⃣ ReentrantReadWriteLock(读写锁)

🔬 原理深度
  • AQS 双状态设计:
    • 高16位:读锁计数(共享)
    • 低16位:写锁计数(独占)
  • 写锁降级 :获取写锁 → 获取读锁 → 释放写锁(安全
  • 读锁升级 :获取读锁 → 尝试写锁(死锁风险!禁止
💻 规范示例
java 复制代码
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.HashMap;
import java.util.Map;

public class CacheWithReadWriteLock {
    private final Map<String, String> cache = new HashMap<>();
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

    // 读操作(高并发)
    public String get(String key) {
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }

    // 写操作(独占)
    public void put(String key, String value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    // 写锁降级示例(安全模式)
    public String computeIfAbsent(String key, java.util.function.Function<String, String> loader) {
        readLock.lock();
        String value = cache.get(key);
        if (value != null) {
            readLock.unlock(); // 有缓存直接返回
            return value;
        }
        readLock.unlock(); // 释放读锁
        
        writeLock.lock(); // 升级为写锁(注意:此处已释放读锁,非直接升级)
        try {
            value = cache.get(key);
            if (value == null) {
                value = loader.apply(key);
                cache.put(key, value);
            }
            // 降级:先获取读锁,再释放写锁
            readLock.lock();
        } finally {
            writeLock.unlock(); // 释放写锁,保留读锁
        }
        try {
            return value;
        } finally {
            readLock.unlock(); // 最终释放读锁
        }
    }
}
⚠️ 重要警告
java 复制代码
// ❌ 危险:读锁内尝试获取写锁 → 死锁!
readLock.lock();
try {
    if (needUpdate) {
        writeLock.lock(); // 永远无法获取,当前线程已持读锁
        // ...
    }
} finally {
    readLock.unlock();
}

4️⃣ StampedLock(邮戳锁|JDK 8+)

🔬 原理深度
  • 票据(Stamp)机制:64 位 long 值标识锁状态

  • 三种模式:

    模式 方法 特性
    写锁 writeLock() 独占,返回 stamp
    悲观读 readLock() 共享,返回 stamp
    乐观读 tryOptimisticRead() 无锁,返回 stamp,需验证
  • 验证机制validate(stamp) 检查期间是否有写入

  • JDK 17 优势 :避免 ReentrantReadWriteLock 的写饥饿问题

💻 规范示例
java 复制代码
import java.util.concurrent.locks.StampedLock;

public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    // 乐观读(高性能路径)
    public double distanceFromOrigin() {
        long stamp = sl.tryOptimisticRead(); // ① 乐观读
        double currentX = x, currentY = y;
        
        // ② 验证:若验证失败(期间有写入),降级为悲观读
        if (!sl.validate(stamp)) {
            stamp = sl.readLock(); // ③ 悲观读锁
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 写操作
    public void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock(); // ① 写锁
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp); // ② 释放
        }
    }

    // 悲观读示例(需保证读期间数据不变)
    public void printCoordinates() {
        long stamp = sl.readLock();
        try {
            System.out.println("Point: (" + x + ", " + y + ")");
        } finally {
            sl.unlockRead(stamp);
        }
    }
}
⚠️ 严格限制
限制 说明
不可重入 同一线程重复获取锁会死锁
无 Condition 不支持 newCondition()
中断不安全 readLock()/writeLock() 不响应中断(用 try... 系列方法)
内存泄漏风险 必须保存 stamp 值用于解锁

5️⃣ JVM 底层锁优化机制(synchronized 底层)

🔬 锁升级全流程(JDK 17 视角)

无锁状态
偏向锁:
第一个线程访问
偏向锁
轻量级锁:
其他线程竞争(撤销偏向)
轻量级锁
重量级锁:
自旋超时/竞争激烈
无锁状态:
释放锁
重量级锁
JDK 17 默认禁用!

启用需:-XX:+UseBiasedLocking

🔍 各阶段详解
阶段 Mark Word 变化 适用场景 JDK 17 状态
无锁 对象哈希码 + 分代年龄 无竞争 常态
偏向锁 线程ID + 偏向时间戳 单线程长期持有 默认禁用
轻量级锁 指向栈中锁记录的指针 短时、低竞争 活跃
重量级锁 指向 Monitor 对象指针 高竞争、长持有 活跃
💡 自旋锁机制(JVM 内部)
  • 原理:线程在用户态循环 CAS 尝试获取锁(避免挂起开销)

  • JDK 17 优化:

    • 自适应自旋:根据历史成功次数动态调整自旋次数
    • Thread.onSpinWait():提示 CPU 优化自旋(JDK 9+)
    java 复制代码
    // 自旋等待示例(配合 volatile 使用)
    volatile boolean flag = false;
    
    public void spinWaitExample() {
        while (!flag) {
            Thread.onSpinWait(); // JDK 9+:提示 CPU 进入自旋优化模式
        }
        // 执行后续操作
    }

6️⃣ 乐观锁(无锁编程)

🔬 原理深度
  • CAS(Compare-And-Swap):
    • 原子指令:compareAndSet(expected, update)
    • 底层:CPU LOCK CMPXCHG 指令(保证原子性)
  • JDK 17 演进:
    • Unsafe 逐步废弃 → VarHandle 成为标准(JDK 9+)
    • Atomic* 类内部已迁移至 VarHandle
💻 规范示例
java 复制代码
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

public class OptimisticLockingExample {
    
    // 方式1:AtomicInteger(封装 CAS)
    private final AtomicInteger counter = new AtomicInteger(0);
    public void atomicIncrement() {
        counter.incrementAndGet(); // 内部 CAS 循环
    }

    // 方式2:VarHandle(JDK 9+ 推荐,替代 Unsafe)
    private volatile long value;
    private static final VarHandle VH;
    static {
        try {
            VH = MethodHandles.lookup()
                .findVarHandle(OptimisticLockingExample.class, "value", long.class);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    public void varHandleIncrement() {
        long expected;
        long update;
        do {
            expected = value;
            update = expected + 1;
        } while (!VH.compareAndSet(this, expected, update)); // CAS
    }

    // 方式3:LongAdder(高并发计数优化)
    private final LongAdder adder = new LongAdder();
    public void highContentionIncrement() {
        adder.increment(); // 分段累加,减少 CAS 冲突
    }
    
    // ABA 问题解决方案:AtomicStampedReference
    // (示例略,适用于需检测值是否被修改过的场景)
}
📊 乐观锁 vs 悲观锁
维度 乐观锁 悲观锁
冲突假设 低冲突 高冲突
阻塞 无(失败重试) 有(挂起线程)
适用场景 计数器、状态标志 数据库事务、复杂业务
ABA 问题 需额外处理(StampedReference) 不存在

7️⃣ 锁支撑工具

🔹 LockSupport(线程阻塞/唤醒基石)
java 复制代码
import java.util.concurrent.locks.LockSupport;

public class LockSupportExample {
    static class Parker extends Thread {
        public void run() {
            System.out.println("Thread waiting...");
            LockSupport.park(); // 挂起(响应中断,但不抛异常)
            if (Thread.interrupted()) {
                System.out.println("Interrupted!");
            }
            System.out.println("Thread resumed");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Parker t = new Parker();
        t.start();
        Thread.sleep(100);
        LockSupport.unpark(t); // 精确唤醒指定线程(无许可计数)
    }
}
  • 原理 :操作线程的 parkBlocker 字段(用于诊断)
  • vs wait/notify :无需持有锁,可先 unparkpark
🔹 Thread.onSpinWait()(JDK 9+)
  • 提示 CPU 当前线程处于自旋等待
  • 优化能效:CPU 可降低功耗或优化流水线
  • 使用场景:自旋锁、无锁队列的等待循环

三、锁选型决策指南

🌐 选型决策流程图

速查表(增强版)

场景 推荐方案 关键代码片段 避坑提示
单例延迟初始化 synchronized + 双重检查 if (instance == null) { synchronized (lock) { ... } } 字段必须 volatile
配置热更新(读99%) StampedLock long s = sl.tryOptimisticRead(); if (!sl.validate(s)) s = sl.readLock(); 验证失败必须降级
订单状态机流转 ReentrantLock + Condition lock.lock(); try { while (!valid) notValid.await(); } 避免虚假唤醒(用while)
分布式ID生成器 LongAdder adder.increment(); return base + adder.sum(); 非强一致性场景
线程安全本地缓存 ConcurrentHashMap map.computeIfAbsent(key, k -> load(k)); 优于 手动加锁Map
定时任务调度器 ScheduledThreadPoolExecutor scheduler.scheduleAtFixedRate(task, 0, 1, SECONDS); 优于 手写锁+wait

💡 选型心法(一句话总结)

"能用并发容器不用锁,能用synchronized不用显式锁,能用乐观锁不用悲观锁,能重构消除共享状态绝不加锁"


四、JDK 17 锁机制实战指南:原则、陷阱与调优

核心思想 :锁是手段而非目的,目标是正确性 > 可维护性 > 性能。以下内容源自千万级并发系统实战经验,直击生产环境痛点。


4.1 🌟 现代锁使用黄金法则(场景化修订)

原则 为什么重要 实战场景示例 反面案例
✅ 简单互斥首选 synchronized 语法安全、JVM深度优化、调试友好 单例双重检查锁、简单计数器 为"性能"强行用ReentrantLock,增加维护成本
✅ 显式锁必须 try-finally 配对 防止异常导致永久死锁 数据库连接池获取连接 lock.lock(); doWork(); lock.unlock();(异常时锁泄漏)
✅ 临界区最小化:锁外计算,锁内赋值 减少线程阻塞时间,提升吞吐 缓存更新:先计算新值,锁内仅替换引用 锁内调用RPC/DB/复杂计算
✅ 多锁场景:全局统一加锁顺序 根本性预防死锁 转账场景:按账户ID升序加锁 任意顺序加锁导致循环等待
✅ 读多写少:StampedLock > ReadWriteLock 避免写饥饿,乐观读提升吞吐 配置中心、实时指标查询 synchronized导致读线程阻塞
✅ 高并发计数:LongAdder 替代 AtomicInteger 分段累加,减少CAS冲突 服务QPS统计、埋点计数 万级QPS下AtomicInteger性能骤降
✅ 无锁编程:仅限简单状态机 避免ABA、内存屏障复杂性 线程状态标志(INIT→RUNNING→STOPPED) 用CAS实现复杂业务逻辑导致隐蔽Bug
✅ 监控先行:上线必埋点 快速定位线上锁竞争 通过Micrometer暴露锁等待时间指标 问题发生后"盲调"

4.2 🔍 高频陷阱与解决方案(附修复代码)

❌ 陷阱1:锁对象逃逸导致意外共享

java 复制代码
// 反面教材:锁字符串常量(所有实例共享同一把锁!)
public void badLock() {
    synchronized ("CONFIG_LOCK") { // 所有类实例竞争同一把锁
        // ...
    }
}

// ✅ 修复方案:使用私有final对象
private final Object configLock = new Object();
public void goodLock() {
    synchronized (configLock) {
        // 仅当前实例加锁
    }
}

❌ 陷阱2:锁内调用外部回调(隐式死锁风险)

java 复制代码
// 反面教材:锁内调用用户回调
public void process(Order order) {
    synchronized (lock) {
        validator.validate(order); // 回调可能持有其他锁!
        // ...
    }
}

// ✅ 修复方案:锁外验证,锁内仅操作内部状态
public void safeProcess(Order order) {
    if (!validator.validate(order)) return; // 锁外验证
    
    synchronized (lock) {
        // 仅操作内部状态
        orders.add(order);
    }
}

❌ 陷阱3:忽略中断导致线程无法优雅退出

java 复制代码
// 反面教材:吞掉中断信号
public void badInterrupt() {
    lock.lock();
    try {
        // ... 
    } finally {
        lock.unlock();
    }
}

// ✅ 修复方案:响应中断 + 恢复中断状态
public void safeInterrupt() throws InterruptedException {
    if (!lock.tryLock(2, TimeUnit.SECONDS)) {
        Thread.currentThread().interrupt(); // 恢复中断状态
        throw new InterruptedException("Lock acquisition timeout");
    }
    try {
        // ...
    } finally {
        lock.unlock();
    }
}

❌ 陷阱4:StampedLock 误用(重入/中断)

java 复制代码
// 反面教材:尝试重入StampedLock(必然死锁!)
public void dangerous() {
    long s = sl.writeLock();
    try {
        long s2 = sl.writeLock(); // 同一线程再次获取 → 永久阻塞!
    } finally {
        sl.unlockWrite(s);
    }
}

// ✅ 修复方案:严格避免重入,或改用ReentrantReadWriteLock

4.3 🛠️ 监控与诊断实战(JDK 17 工具链)

🔹 锁竞争热点定位

bash 复制代码
# 1. 生成线程转储(定位死锁/阻塞)
jstack -l <pid> > thread_dump.log
# 查看 "java.lang.Thread.State: BLOCKED" 线程及锁持有者

# 2. Async-Profiler 火焰图(精准定位锁等待)
async-profiler/profiler.sh -e lock -d 30 -f lock.svg <pid>
# 生成锁等待火焰图,直观看到热点锁

# 3. JFR(Java Flight Recorder)记录锁事件(JDK 17内置)
java -XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,filename=recording.jfr MyApp
# 用 JDK Mission Control (JMC) 分析:Lock Instances, Monitor Blocked 等事件

🔹 Micrometer 监控埋点(Spring Boot 示例)

java 复制代码
@Component
public class LockMetrics {
    private final Counter lockTimeouts;
    private final Timer lockAcquisitionTime;
    
    public LockMetrics(MeterRegistry registry) {
        lockTimeouts = Counter.builder("lock.timeouts")
            .description("Lock acquisition timeout count")
            .register(registry);
        lockAcquisitionTime = Timer.builder("lock.acquisition.time")
            .description("Time taken to acquire lock")
            .register(registry);
    }
    
    public <T> T executeWithMetrics(Supplier<T> task, Lock lock) {
        long start = System.nanoTime();
        boolean acquired = false;
        try {
            acquired = lock.tryLock(2, TimeUnit.SECONDS);
            if (!acquired) {
                lockTimeouts.increment();
                throw new RuntimeException("Lock timeout");
            }
            return task.get();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        } finally {
            if (acquired) {
                lockAcquisitionTime.record(System.nanoTime() - start, TimeUnit.NANOSECONDS);
                lock.unlock();
            }
        }
    }
}

4.4 🧪 并发测试策略(不止于 jcstress)

工具 适用场景 示例命令
jcstress 验证内存可见性、指令重排 mvn jcstress:run -Djcstress.forks=4
ThreadSanitizer (TSan) 检测数据竞争(需GraalVM Native Image) native-image --enable-thread-sanitizer ...
Chaos Engineering 模拟高负载/网络延迟下的锁行为 使用 Chaos Mesh 注入延迟
压测验证 验证锁在峰值流量下的表现 Gatling/JMeter + 监控锁指标

jcstress 简单示例(验证可见性)

java 复制代码
@JCStressTest
@Outcome(id = "1, 1", expect = Acceptable, desc = "Correct publication")
@State
public class SafePublicationTest {
    int x;
    volatile boolean ready; // 关键:volatile 保证可见性

    @Actor
    public void actor1() {
        x = 1;
        ready = true;
    }

    @Actor
    public void actor2(IntResult2 r) {
        r.r1 = ready ? x : -1;
        r.r2 = x;
    }
}

4.5 📌 JDK 17 专属实践建议

场景 建议 原因
新项目开发 直接忽略偏向锁 JDK 17 默认禁用,无需配置
遗留系统迁移 测试 -XX:-UseBiasedLocking 影响 避免因默认禁用导致性能波动
原子操作 优先 VarHandle > Unsafe Unsafe 已标记 @Deprecated(forRemoval=true)
自旋优化 短等待循环中加入 Thread.onSpinWait() 提示CPU优化流水线,降低功耗
容器环境 设置 -XX:ActiveProcessorCount 避免JVM误判CPU核数影响自旋策略
云原生应用 结合 Kubernetes livenessProbe 设置超时 避免锁等待导致Pod被误杀

4.6 💡 终极心法:何时该用锁?何时不该用?

问题 决策路径
需要互斥访问共享可变状态? → 是:选锁;否:考虑不可变对象/线程局部变量
临界区是否包含I/O或远程调用? → 是:重构!将I/O移出锁;否:继续评估
能否用并发容器替代? ConcurrentHashMap > 手动加锁Map;BlockingQueue > 手动实现生产者-消费者
是否可接受最终一致性? → 是:考虑异步队列(如Disruptor)、事件溯源
锁竞争是否成为瓶颈? → 用Async-Profiler验证;是:考虑分段锁、无锁结构、架构拆分

记住

🔸 最好的锁是不需要锁 (通过架构设计消除共享状态)

🔸 次好的锁是看不见的锁 (JVM优化后的synchronized)

🔸 最差的锁是"聪明"的锁(过度设计、难以维护的锁逻辑)

相关推荐
小北方城市网2 小时前
Redis 分布式锁高可用实现:从原理到生产级落地
java·前端·javascript·spring boot·redis·分布式·wpf
六义义3 小时前
java基础十二
java·数据结构·算法
毕设源码-钟学长3 小时前
【开题答辩全过程】以 基于SpringBoot的智能书城推荐系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
笨手笨脚の4 小时前
深入理解 Java 虚拟机-03 垃圾收集
java·jvm·垃圾回收·标记清除·标记复制·标记整理
莫问前路漫漫4 小时前
WinMerge v2.16.41 中文绿色版深度解析:文件对比与合并的全能工具
java·开发语言·python·jdk·ai编程
九皇叔叔4 小时前
【03】SpringBoot3 MybatisPlus BaseMapper 源码分析
java·开发语言·mybatis·mybatis plus
挖矿大亨4 小时前
c++中的函数模版
java·c++·算法
a程序小傲5 小时前
得物Java面试被问:RocketMQ的消息轨迹追踪实现
java·linux·spring·面试·职场和发展·rocketmq·java-rocketmq
青春男大5 小时前
Redis和RedisTemplate快速上手
java·数据库·redis·后端·spring·缓存
Ghost Face...5 小时前
i386 CPU页式存储管理深度解析
java·linux·服务器