ReentrantReadWriteLock 简介

ReentrantReadWriteLock 是 Java 中 java.util.concurrent.locks 包提供的一个可重入的读写锁,它允许多个读线程同时访问共享资源,但在写线程存在时不允许任何读线程或写线程访问。这种机制非常适合读多写少的场景。


一、基本概念

1. 什么是读写锁?

  • 读锁(Read Lock):多个线程可以同时获取读锁(共享锁),适用于只读操作。
  • 写锁(Write Lock):只有一个线程能获取写锁(排他锁),适用于修改数据的操作。

在并发编程中,如果多个线程都只是读取共享变量而不进行修改,则无需加锁互斥;只有在有线程要修改数据时才需要独占锁。


二、特性

  1. 锁分离机制

    • 读锁:共享,不互斥(多个线程可同时持有)
    • 写锁:独占,互斥(与其他读/写锁互斥)
  2. 锁降级

    writeLock.lock();      // 1. 获取写锁
    readLock.lock();       // 2. 获取读锁(锁降级开始)
    writeLock.unlock();    // 3. 释放写锁(降级完成)
    // 此时仍持有读锁
    
    • 允许一个线程先持有写锁再获取读锁
    • 禁止锁升级(读锁 → 写锁)可能引发死锁
  3. 公平性选择

    • 公平模式:按等待队列顺序获取锁
    • 非公平模式(默认):允许插队提高吞吐量
  4. 可重入性

    • 同一线程可重复获取读锁/写锁

三、内部结构

ReentrantReadWriteLock 内部有两个子类:

  • ReentrantReadWriteLock.ReadLock
  • ReentrantReadWriteLock.WriteLock

通过一个 int 状态值(AQS 的 state 变量)同时维护两种锁:

  • 高16位:读锁持有数量(包括重入次数)
  • 低16位:写锁重入次数
static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

// 读锁计数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 写锁计数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

四、示例

基础使用

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Cache {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

    private String data = "Initial Data"; // 模拟缓存数据

    // 读取数据
    public void readData() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在读取数据: " + data);
            Thread.sleep(500); // 模拟耗时操作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            readLock.unlock();
        }
    }

    // 更新数据
    public void writeData(String newData) {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在写入数据: " + newData);
            data = newData;
            Thread.sleep(1000); // 模拟耗时写操作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        Cache cache = new Cache();

        // 创建多个读线程
        for (int i = 0; i < 5; i++) {
            new Thread(cache::readData, "Reader-" + i).start();
        }

        // 创建一个写线程
        new Thread(() -> cache.writeData("New Updated Data"), "Writer").start();
    }
}

说明:

  • 所有 Reader 线程几乎同时执行读操作。
  • Writer 线程必须等待所有读线程释放锁后才能执行。
  • 写完成后,后续的读线程将读到新值。

缓存系统

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

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

    // 读操作:使用读锁(共享)
    public V get(K key) {
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }

    // 写操作:使用写锁(独占)
    public void put(K key, V value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    // 锁降级:先更新再持续读取
    public void updateAndRead(K key, V newValue) {
        writeLock.lock();
        try {
            // 1. 更新数据(持有写锁)
            cache.put(key, newValue);
            
            // 2. 获取读锁(锁降级开始)
            readLock.lock();
        } finally {
            writeLock.unlock(); // 3. 释放写锁(降级完成)
        }

        try {
            // 4. 仍持有读锁,安全读取
            System.out.println("Current value: " + cache.get(key));
        } finally {
            readLock.unlock();
        }
    }
}

五、锁降级

允许写锁降级为读锁,这在某些场景中非常有用,例如:

writeLock.lock();
try {
    // 修改数据
    doUpdate();

    // 降级为读锁
    readLock.lock();
} finally {
    writeLock.unlock(); // 释放写锁,但保留读锁
}

try {
    // 使用新数据做只读操作
    useData();
} finally {
    readLock.unlock();
}

注意:不能反过来,即不允许从读锁升级到写锁。


六、公平性设置

构造时可以传入布尔参数决定是否启用公平锁:

ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); // true 表示公平模式
  • 非公平模式(默认):吞吐量更高,但可能造成“饥饿”。
  • 公平模式:线程按请求顺序获得锁,避免饥饿,但吞吐量较低。

七、常见问题

1. 读写锁和 synchronized 对比?

特性synchronizedReentrantReadWriteLock
支持读写分离
支持尝试获取锁✅(tryLock)
支持超时获取锁
支持中断响应
可重入
性能优化(读多写少)

2. 为什么不能从读锁升级到写锁?

因为多个线程可能同时持有读锁,如果其中一个线程试图升级为写锁,就会破坏其他读线程的数据一致性。


八、适用场景

场景是否推荐使用 ReentrantReadWriteLock
读多写少✅ 强烈推荐
读写频率相当⚠️ 视情况而定,注意锁竞争
写多读少❌ 效率不高,建议使用普通锁
需要锁降级✅ 优势明显
需要公平调度✅ 可开启公平模式

九、建议

  • 如果场景是读很多且写极少,可以考虑使用 StampedLock,它是 JDK8 新增的更高效的读写锁实现。
  • 如果使用的是 Spring 或者其它框架,也可以结合 AOP 来管理锁逻辑。
  • 在分布式系统中,Java 原生锁不再适用,应使用分布式锁如 Redis、Zookeeper等。

十、注意事项

  1. 写锁饥饿问题

    • 非公平模式下,大量读线程可能导致写线程长期等待
    • 解决方案:使用公平锁或 StampedLock
  2. 锁升级禁止

    // 错误示例(导致死锁):
    readLock.lock();
    try {
        writeLock.lock();  // 阻塞等待其他读锁释放
    } finally {
        readLock.unlock();
    }
    
  3. 锁释放匹配

    • 获取多少次锁就释放多少次
  4. 中断

    • 写锁支持获取过程中响应中断
    • 读锁不支持中断(lockInterruptibly() 除外)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值