Java中的读写锁(ReadWriteLock)与StampedLock详解
字数 1748 2025-12-11 11:11:18

Java中的读写锁(ReadWriteLock)与StampedLock详解


一、读写锁(ReadWriteLock)简介

读写锁是一种特殊的锁机制,允许多个线程同时读取共享资源,但只允许一个线程进行写操作。这种设计能显著提升读多写少场景下的并发性能。

核心思想

  • 读锁(共享锁):可被多个线程同时持有
  • 写锁(排他锁):同一时刻只能被一个线程持有
  • 读写互斥:有写锁时,读锁被阻塞;有读锁时,写锁被阻塞

二、ReadWriteLock接口与ReentrantReadWriteLock实现

1. 接口定义

public interface ReadWriteLock {
    Lock readLock();  // 获取读锁
    Lock writeLock(); // 获取写锁
}

2. ReentrantReadWriteLock特性

  • 支持公平/非公平模式
  • 可重入性:线程可重复获取已持有的锁
  • 锁降级:写锁可降级为读锁(反之不行)
  • 锁升级:不支持读锁升级为写锁(会死锁)

3. 使用示例

class DataCache {
    private final Map<String, Object> map = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    
    public Object get(String key) {
        rwLock.readLock().lock();  // 获取读锁
        try {
            return map.get(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    public void put(String key, Object value) {
        rwLock.writeLock().lock(); // 获取写锁
        try {
            map.put(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

三、读写锁的实现原理

1. 状态设计

ReentrantReadWriteLock使用一个32位int同时维护读锁和写锁状态:

  • 高16位:读锁计数(共享线程数)
  • 低16位:写锁计数(可重入次数)

2. 锁获取规则

  • 写锁获取条件
    • 无任何锁(state == 0)
    • 当前线程已持有写锁(可重入)
  • 读锁获取条件
    • 无写锁(state低16位为0)
    • 公平模式下检查等待队列

3. 关键方法源码解析

// 写锁尝试获取
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();  // 当前锁状态
    int w = exclusiveCount(c);  // 写锁数量
    
    if (c != 0) {  // 有锁存在
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;  // 存在读锁 或 写锁被其他线程持有
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
    }
    
    if (writerShouldBlock() ||  // 公平性检查
        !compareAndSetState(c, c + acquires))
        return false;
    
    setExclusiveOwnerThread(current);
    return true;
}

四、读写锁的局限性

  1. 写锁饥饿:大量读线程可能导致写线程长时间等待
  2. 性能下降:高竞争时CAS操作频繁
  3. 不支持乐观读:读操作也需要获取锁

五、StampedLock的引入

Java 8引入的增强型读写锁,解决了传统读写锁的部分问题。

1. 三种访问模式

  • 写锁(Writing):独占锁,类似ReentrantReadWriteLock.WriteLock
  • 悲观读锁(Reading):共享锁,类似ReentrantReadWriteLock.ReadLock
  • 乐观读(Optimistic Reading):无锁操作,通过验证确保数据一致性

2. 核心优势

  • 乐观读避免锁开销
  • 支持锁升级/降级
  • 更好的吞吐量

六、StampedLock使用详解

1. 基本用法

class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
    
    // 写操作
    void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();  // 获取写锁
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);  // 释放写锁
        }
    }
    
    // 悲观读
    double distanceFromOrigin() {
        long stamp = sl.readLock();  // 获取读锁
        try {
            return Math.sqrt(x * x + y * y);
        } finally {
            sl.unlockRead(stamp);
        }
    }
    
    // 乐观读
    double distanceFromOriginOptimistic() {
        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);
    }
}

2. 锁转换示例

// 读锁升级为写锁
long stamp = sl.readLock();
try {
    while (condition) {
        long ws = sl.tryConvertToWriteLock(stamp);
        if (ws != 0L) {
            stamp = ws;
            // 执行写操作
            break;
        } else {
            sl.unlockRead(stamp);
            stamp = sl.writeLock();
        }
    }
} finally {
    sl.unlock(stamp);
}

七、StampedLock实现原理

1. 状态设计

使用long类型state维护状态:

  • 低7位:写锁状态和读锁计数
  • 第8位:写锁占用标志
  • 高56位:读锁计数和版本号

2. 乐观读实现机制

public long tryOptimisticRead() {
    long s = state;
    // 返回版本号(高56位),如果低8位有写锁则返回0
    return (s & WBIT) == 0L ? (s & RBITS) : 0L;
}

public boolean validate(long stamp) {
    // 比较当前状态与乐观读时获取的版本号
    // 通过内存屏障确保读取顺序
    U.loadFence();
    return (stamp & SBITS) == (state & SBITS);
}

3. 内存屏障使用

  • loadFence():确保validate前的加载操作不会重排序
  • storeFence():确保写锁释放前的存储操作可见

八、对比总结

特性 ReentrantReadWriteLock StampedLock
锁类型 读锁、写锁 读锁、写锁、乐观读
可重入 支持 不支持
锁升级 不支持 支持
公平模式 支持 不支持
条件变量 支持 不支持
性能 中等 高并发下更好
死锁风险 较低 使用不当易死锁

九、使用建议与注意事项

1. 选择依据

  • 选ReentrantReadWriteLock
    • 需要可重入特性
    • 需要条件变量支持
    • 代码复杂度要求低
  • 选StampedLock
    • 读多写少且读操作耗时短
    • 对性能要求极高
    • 能妥善处理锁转换

2. 注意事项

  1. StampedLock不可重入:同一线程重复获取会死锁
  2. 必须释放锁:finally块中确保unlock
  3. 避免长时间持有锁:特别是写锁
  4. 验证乐观读:必须调用validate检查
  5. 小心锁转换:转换失败需正确处理

3. 最佳实践示例

class CachedData {
    private Object data;
    private final StampedLock lock = new StampedLock();
    
    public Object read() {
        // 1. 尝试乐观读
        long stamp = lock.tryOptimisticRead();
        Object currentData = data;
        
        // 2. 验证数据一致性
        if (!lock.validate(stamp)) {
            // 3. 验证失败,升级为悲观读
            stamp = lock.readLock();
            try {
                currentData = data;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        return currentData;
    }
}

十、总结

读写锁通过分离读/写操作提升了并发性能,而StampedLock通过乐观读机制进一步优化。在实际开发中,应根据具体场景选择:

  • 简单场景用ReentrantReadWriteLock
  • 高性能场景用StampedLock
  • 始终注意锁的获取顺序和释放,避免死锁

这两种锁都是Java并发工具包中的重要组件,理解其原理和适用场景,能帮助我们在实际开发中做出更合适的技术选型。

Java中的读写锁(ReadWriteLock)与StampedLock详解 一、读写锁(ReadWriteLock)简介 读写锁 是一种特殊的锁机制,允许多个线程同时读取共享资源,但只允许一个线程进行写操作。这种设计能显著提升读多写少场景下的并发性能。 核心思想 : 读锁(共享锁):可被多个线程同时持有 写锁(排他锁):同一时刻只能被一个线程持有 读写互斥:有写锁时,读锁被阻塞;有读锁时,写锁被阻塞 二、ReadWriteLock接口与ReentrantReadWriteLock实现 1. 接口定义 2. ReentrantReadWriteLock特性 支持公平/非公平模式 可重入性:线程可重复获取已持有的锁 锁降级:写锁可降级为读锁(反之不行) 锁升级:不支持读锁升级为写锁(会死锁) 3. 使用示例 三、读写锁的实现原理 1. 状态设计 ReentrantReadWriteLock使用一个32位int同时维护读锁和写锁状态: 高16位:读锁计数(共享线程数) 低16位:写锁计数(可重入次数) 2. 锁获取规则 写锁获取条件 : 无任何锁(state == 0) 当前线程已持有写锁(可重入) 读锁获取条件 : 无写锁(state低16位为0) 公平模式下检查等待队列 3. 关键方法源码解析 四、读写锁的局限性 写锁饥饿 :大量读线程可能导致写线程长时间等待 性能下降 :高竞争时CAS操作频繁 不支持乐观读 :读操作也需要获取锁 五、StampedLock的引入 Java 8引入的增强型读写锁,解决了传统读写锁的部分问题。 1. 三种访问模式 写锁(Writing) :独占锁,类似ReentrantReadWriteLock.WriteLock 悲观读锁(Reading) :共享锁,类似ReentrantReadWriteLock.ReadLock 乐观读(Optimistic Reading) :无锁操作,通过验证确保数据一致性 2. 核心优势 乐观读避免锁开销 支持锁升级/降级 更好的吞吐量 六、StampedLock使用详解 1. 基本用法 2. 锁转换示例 七、StampedLock实现原理 1. 状态设计 使用long类型state维护状态: 低7位:写锁状态和读锁计数 第8位:写锁占用标志 高56位:读锁计数和版本号 2. 乐观读实现机制 3. 内存屏障使用 loadFence():确保validate前的加载操作不会重排序 storeFence():确保写锁释放前的存储操作可见 八、对比总结 | 特性 | ReentrantReadWriteLock | StampedLock | |------|----------------------|-------------| | 锁类型 | 读锁、写锁 | 读锁、写锁、乐观读 | | 可重入 | 支持 | 不支持 | | 锁升级 | 不支持 | 支持 | | 公平模式 | 支持 | 不支持 | | 条件变量 | 支持 | 不支持 | | 性能 | 中等 | 高并发下更好 | | 死锁风险 | 较低 | 使用不当易死锁 | 九、使用建议与注意事项 1. 选择依据 选ReentrantReadWriteLock : 需要可重入特性 需要条件变量支持 代码复杂度要求低 选StampedLock : 读多写少且读操作耗时短 对性能要求极高 能妥善处理锁转换 2. 注意事项 StampedLock不可重入 :同一线程重复获取会死锁 必须释放锁 :finally块中确保unlock 避免长时间持有锁 :特别是写锁 验证乐观读 :必须调用validate检查 小心锁转换 :转换失败需正确处理 3. 最佳实践示例 十、总结 读写锁通过分离读/写操作提升了并发性能,而StampedLock通过乐观读机制进一步优化。在实际开发中,应根据具体场景选择: 简单场景用ReentrantReadWriteLock 高性能场景用StampedLock 始终注意锁的获取顺序和释放,避免死锁 这两种锁都是Java并发工具包中的重要组件,理解其原理和适用场景,能帮助我们在实际开发中做出更合适的技术选型。