Java 锁全景:synchronized、ReentrantLock、ReadWriteLock、StampedLock

Java 锁全景:synchronized、ReentrantLock、ReadWriteLock、StampedLock

Java 中有四种主流锁机制,每种都有适用场景。选错锁,性能差十倍。本文对比四种锁的原理和选型指南。

一、Java 锁全景图

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Java 锁全景                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────┐  互斥锁  ┌──────────────────────────┐  │
│  │ synchronized    │         │ ReentrantLock             │  │
│  │ (JVM 内置)      │         │ (AQS 实现)                │  │
│  │ 偏向→轻量→重量  │         │ 公平/非公平                │  │
│  └─────────────────┘         └──────────────────────────┘  │
│                                                             │
│  ┌─────────────────┐  读写锁  ┌──────────────────────────┐  │
│  │ ReadWriteLock   │         │ StampedLock (JDK 8+)      │  │
│  │ 读写分离         │         │ 乐观读 + 读写分离          │  │
│  │ 读读共享         │         │ 读不加锁(乐观) → 回退      │  │
│  │ 读写互斥         │         │ 读写互斥                  │  │
│  └─────────────────┘         └──────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二、synchronized

java 复制代码
// 互斥锁,JVM 内置
public class SyncDemo {
    private int count = 0;

    // 锁 this
    public synchronized void increment() {
        count++;
    }

    // 锁指定对象
    public void method() {
        synchronized(this) {
            count++;
        }
    }

    // 锁 Class
    public static synchronized void staticMethod() {
        // 锁 Class 对象
    }
}
arduino 复制代码
synchronized 特点:
  优点: 简单,JVM 管理,自动释放
  缺点: 不可中断,不可超时,单条件变量
  锁升级: 偏向锁 → 轻量级锁 → 重量级锁
  适用: 竞争不激烈的场景

三、ReentrantLock

java 复制代码
// 可重入锁,AQS 实现
public class ReentrantLockDemo {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock(); // 必须手动释放
        }
    }

    // 公平锁 (FIFO)
    private final ReentrantLock fairLock = new ReentrantLock(true);

    // 可中断加锁
    public void interruptibleLock() throws InterruptedException {
        fairLock.lockInterruptibly(); // 可被 interrupt 打断
        try {
            // 临界区
        } finally {
            fairLock.unlock();
        }
    }

    // 超时加锁
    public boolean tryLockWithTimeout() throws InterruptedException {
        if (lock.tryLock(3, TimeUnit.SECONDS)) {
            try {
                // 获取锁成功
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false; // 超时未获取
    }

    // 多条件变量
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public void awaitCondition() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await(); // 等待 notEmpty 条件
            }
            count--;
            notFull.signal(); // 通知 notFull 条件
        } finally {
            lock.unlock();
        }
    }
}
objectivec 复制代码
ReentrantLock 特点:
  优点: 可中断、可超时、公平锁、多条件变量
  缺点: 必须手动释放 (忘记 unlock → 死锁)
  实现: AQS (CLH 队列 + state)
  适用: 需要高级锁功能的场景

公平 vs 非公平

java 复制代码
// 非公平锁 (默认): 新来的线程先尝试插队
ReentrantLock lock = new ReentrantLock(false); // 非公平

// 公平锁: 严格按 FIFO 排队
ReentrantLock lock = new ReentrantLock(true); // 公平

// 非公平锁的 tryAcquire:
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 不检查队列! 直接 CAS 获取
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // ...
}

// 公平锁的 tryAcquire:
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 先检查队列中是否有等待者!
        if (!hasQueuedPredecessors() &&  // ← 关键区别
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // ...
}
对比维度 非公平锁 公平锁
吞吐量 高 低
饥饿 可能 不会
上下文切换 少 多
实时性 不保证 严格 FIFO

四、ReadWriteLock

java 复制代码
// 读写分离锁: 读读共享,读写互斥
public class CacheDemo {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
    private Map<String, Object> cache = new HashMap<>();

    // 读操作: 多个线程可同时读
    public Object get(String key) {
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }

    // 写操作: 独占,阻塞所有读和写
    public void put(String key, Object value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}
csharp 复制代码
ReadWriteLock 读写规则:

  ┌──────────────┬──────────────┬──────────────┐
  │              │  读锁(共享)    │  写锁(独占)    │
  ├──────────────┼──────────────┼──────────────┤
  │  读锁(共享)    │  ✅ 共享      │  ❌ 互斥      │
  │  写锁(独占)    │  ❌ 互斥      │  ❌ 互斥      │
  └──────────────┴──────────────┴──────────────┘

  场景:
  Thread1: readLock.lock() ─┐
  Thread2: readLock.lock() ─┤── 同时读,不阻塞
  Thread3: readLock.lock() ─┘
  Thread4: writeLock.lock() ── 等待所有读锁释放
  
  Thread4: writeLock.lock() ── 独占写
  Thread5: readLock.lock() ─── 等待写锁释放

AQS 实现读写锁

perl 复制代码
ReentrantReadWriteLock 底层 (AQS state 分解):

  AQS state (int, 32 bit) 分为两部分:
  ┌─────────────────────────────┐
  │  高 16 位: 读锁持有数 (shared) │
  │  低 16 位: 写锁重入数 (exclusive)│
  └─────────────────────────────┘

  读锁获取:
    state 高 16 位 + 1
    如果低 16 位 ≠ 0 (有写锁) → 等待

  写锁获取:
    state 低 16 位 + 1
    如果 state ≠ 0 (有读锁或写锁) → 等待

  释放:
    读锁释放: 高 16 位 - 1
    写锁释放: 低 16 位 - 1

ReadWriteLock 的写饥饿问题

csharp 复制代码
写饥饿: 读锁持有时间过长,写锁一直无法获取

  Thread1: readLock.lock()  ─┐
  Thread2: readLock.lock()   │── 不断有新读线程进来
  Thread3: readLock.lock()   │
  Thread4: readLock.lock()  ─┘
  Thread5: writeLock.lock() ──── 永远等不到!

  解决: ReentrantReadWriteLock 默认非公平
  → 新读线程可能插队 → 写线程饥饿
  → 用公平模式可以缓解: new ReentrantReadWriteLock(true)
  → 但吞吐量下降

五、StampedLock(JDK 8+)

java 复制代码
// StampedLock: 乐观读 + 读写分离
// 解决 ReadWriteLock 的写饥饿问题
public class StampedLockDemo {
    private final StampedLock sl = new StampedLock();
    private double x, y;

    // 乐观读: 不加锁!
    public double distanceFromOrigin() {
        long stamp = sl.tryOptimisticRead(); // 乐观读 stamp
        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 moveIfAt(double oldX, double oldY, double newX, double newY) {
        long stamp = sl.readLock();
        try {
            while (x == oldX && y == oldY) {
                long ws = sl.tryConvertToWriteLock(stamp); // 尝试升级为写锁
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            sl.unlock(stamp);
        }
    }

    // 写锁
    public void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
}
kotlin 复制代码
StampedLock 三种模式:

  ┌─────────────────────────────────────────────────────┐
  │  1. 乐观读 (tryOptimisticRead + validate)              │
  │     不加锁,读完后验证是否有写操作                       │
  │     如果无写 → 直接用 (零开销!)                        │
  │     如果有写 → 升级为悲观读                             │
  │     适合: 读远多于写,且能容忍偶尔重读                  │
  │                                                     │
  │  2. 悲观读 (readLock)                                 │
  │     与 ReadWriteLock 的读锁类似                        │
  │     读读共享,读写互斥                                 │
  │                                                     │
  │  3. 写锁 (writeLock)                                  │
  │     独占,阻塞所有读和写                               │
  └─────────────────────────────────────────────────────┘

  乐观读流程:
    stamp = tryOptimisticRead()  // 获取 stamp (无锁)
    读数据 x, y                  // 读取共享变量
    if (validate(stamp))         // 验证读期间是否有写
        return compute(x, y)     // 无写 → 直接用! (最优路径)
    else
        readLock() → 重读 → unlockRead() // 有写 → 悲观重试

StampedLock 注意事项

java 复制代码
// ⚠️ StampedLock 不可重入!
StampedLock sl = new StampedLock();
long stamp = sl.readLock();
try {
    method(); // 如果 method 内部又获取读锁 → 死锁!
} finally {
    sl.unlockRead(stamp);
}

// ⚠️ 不要在乐观读和悲观读之间调用可能阻塞的方法
public double badMethod() {
    long stamp = sl.tryOptimisticRead();
    double x = this.x;
    Thread.sleep(100); // ❌ 乐观读期间不要阻塞!
    double y = this.y;
    if (!sl.validate(stamp)) {
        // 可能总是失败
    }
    return Math.sqrt(x*x + y*y);
}

// ⚠️ 虚拟线程中不要用 StampedLock
// StampedLock 在 readLock 时可能触发 park
// 会 pin 虚拟线程

六、四种锁对比

对比维度 synchronized ReentrantLock ReadWriteLock StampedLock
类型 互斥 互斥 读写分离 乐观读+读写
可重入 ✅ ✅ ✅ ❌
公平锁 ❌ ✅ ✅ ❌
可中断 ❌ ✅ ✅ ❌
超时 ❌ ✅ ✅ ❌(乐观读)
条件变量 1(wait/notify) 多个 Condition 多个 ❌
读读共享 ❌ ❌ ✅ ✅(乐观读)
乐观读 ❌ ❌ ❌ ✅
虚拟线程兼容 ⚠️(pin) ✅ ✅ ⚠️
适用场景 简单互斥 高级互斥 读多写少 读远多于写

性能对比

arduino 复制代码
性能对比 (读多写少场景):

  ┌────────────────────────────────────────────────┐
  │  并发读吞吐量 (越高越好)                           │
  │                                                │
  │  synchronized    █                            │  1x
  │  ReentrantLock   ██                           │  1.2x
  │  ReadWriteLock   ██████████████                │  8x (读读共享)
  │  StampedLock     ██████████████████████████    │  20x (乐观读无锁)
  │                                                │
  └────────────────────────────────────────────────┘

  读多写少 + 高并发 → StampedLock 最优
  读写均衡 → ReadWriteLock
  写多读少 → ReentrantLock 或 synchronized
  简单场景 → synchronized

七、选型指南

java 复制代码
锁选型决策树:

  场景分析:
  ├── 纯互斥 (不需要读写分离)?
  │   ├── 竞争不激烈 → synchronized
  │   ├── 需要可中断/超时/公平 → ReentrantLock
  │   └── 虚拟线程场景 → ReentrantLock (避免 synchronized pin)
  │
  └── 读写分离 (读多写少)?
      ├── 读写比例 ~1:1 → ReadWriteLock
      ├── 读远多于写 (10:1+) → StampedLock
      └── 不需要严格一致性 → 乐观读模式

  特殊场景:
    虚拟线程: 避免 synchronized 和 StampedLock (会 pin 线程)
    高并发读: StampedLock 乐观读 (无锁最快)
    需要条件变量: ReentrantLock + Condition
    简单加锁: synchronized (JVM 优化后性能足够)

八、锁的常见陷阱

1. 锁对象不一致

java 复制代码
// ❌ 锁的不是同一个对象
class BadLock {
    public void method1() {
        synchronized(this) { ... }  // 锁 this
    }
    public void method2() {
        synchronized("lock") { ... } // 锁字符串常量!
    }
    public void method3() {
        Object lock = new Object();
        synchronized(lock) { ... }    // 每次新建对象 → 无锁效果
    }
}

// ✅ 统一锁对象
class GoodLock {
    private final Object lock = new Object();
    public void method1() {
        synchronized(lock) { ... }
    }
}

2. 锁泄漏

java 复制代码
// ❌ ReentrantLock 忘记 unlock
public void badMethod() {
    lock.lock();
    if (condition) {
        return; // ❌ 没有 unlock! 锁泄漏!
    }
    lock.unlock();
}

// ✅ try-finally
public void goodMethod() {
    lock.lock();
    try {
        if (condition) return; // unlock 在 finally 中执行
    } finally {
        lock.unlock();
    }
}

3. Double-Checked Locking

java 复制代码
// ❌ 没有 volatile 的 DCL
class Singleton {
    private static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 指令重排!
                }
            }
        }
        return instance;
    }
}

// ✅ 必须加 volatile
class Singleton {
    private static volatile Singleton instance;
    // ...
}

九、常见面试追问

Q1:synchronized 和 ReentrantLock 怎么选?

简单场景用 synchronized(JVM 管理,自动释放,锁升级优化),需要高级功能(可中断、超时、公平、多条件)用 ReentrantLock。虚拟线程场景避免 synchronized(会 pin 线程)。

Q2:StampedLock 为什么快?

乐观读不加锁,完全无锁读取。读多写少场景下,大部分读操作走乐观读路径(tryOptimisticRead + validate),没有 CAS 和阻塞开销。

Q3:ReadWriteLock 有什么问题?

写饥饿:非公平模式下,不断有新读线程获取读锁,写锁可能永远等不到。可以用公平模式缓解但吞吐量下降。StampedLock 的乐观读解决了这个问题。

Q4:什么是锁的公平性?

公平锁严格按 FIFO 顺序获取锁,不会饥饿但吞吐量低。非公平锁允许新线程插队(先尝试 CAS),吞吐量高但可能饥饿。

十、总结

yaml 复制代码
Java 四种锁:

  synchronized:   简单互斥,JVM 优化,自动释放
  ReentrantLock:  高级互斥,可中断/超时/公平/多条件
  ReadWriteLock:  读写分离,读读共享,写饥饿风险
  StampedLock:    乐观读+读写分离,读最优性能

  选型:
    简单互斥 → synchronized
    高级互斥 → ReentrantLock
    读多写少 → ReadWriteLock
    读远多于写 → StampedLock (乐观读)
    虚拟线程 → ReentrantLock (避免 synchronized pin)

锁的选择没有银弹,核心是 根据读写比例、竞争程度和功能需求选择最合适的锁。理解每种锁的底层实现和适用场景,才能在面试和实战中做出正确选择。

相关推荐
郭小李1 小时前
spring @PathVariable 注解用法
java·后端·spring
鱼宵1 小时前
CSV 怎么做销售报表?Stream 第 9 课:读洗聚排打五段管道一次跑通
java·报表·stream
jaysee-sjc1 小时前
【苍穹外卖】Day12:工作台数据看板与 Excel 报表导出
java·开发语言·数据库·spring boot·spring·微信·excel
w_zero_one1 小时前
算法--桶思想
java·开发语言·算法
MetaLite1 小时前
Spring 同一个类里调用 @CacheEvict,为什么缓存没清掉?
java·spring·缓存
sunburn-2 小时前
数据库聚合查询完整教程:从基础函数到实战统计
java·数据库·mysql·数据库开发
wangchunyu1142 小时前
JAVA 类加载机制与双亲委派模型
java·开发语言
小蒜学长2 小时前
基于 Spring Boot 的英语单词背诵系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端
lambo mercy2 小时前
持久化01——线程级持久化
java·开发语言·jvm