Java案例如何实现读写锁?

wen python案例 2

本文目录导读:

Java案例如何实现读写锁?

  1. 目录导读
  2. 读写锁的核心概念与适用场景
  3. Java内置读写锁:ReentrantReadWriteLock(含完整案例)
  4. 手写一个简易读写锁(自定义实现)
  5. 读写锁常见陷阱与性能优化
  6. Q&A:高频面试题与实战解答
  7. 总结:何时用读写锁,何时不用

Java案例详解:如何实现高性能读写锁(ReadWriteLock)实战指南

目录导读

  1. 读写锁的核心概念与适用场景
  2. Java内置读写锁:ReentrantReadWriteLock(含案例)
  3. 手写一个简易读写锁(自定义实现)
  4. 读写锁常见陷阱与性能优化
  5. Q&A:高频面试题与实战解答
  6. 何时用读写锁,何时不用

读写锁的核心概念与适用场景

在高并发编程中,锁是保证数据一致性的关键工具,但传统的 synchronizedReentrantLock 是“互斥锁”——所有线程(读或写)都只能串行执行,在实际业务中,读操作远多于写操作(比如缓存、配置中心、读多写少的数据库连接池),如果读线程之间也互斥,会严重浪费性能。

读写锁(ReadWriteLock) 正是为了解决这个问题而设计的:

  • 读锁(共享锁):多个线程可以同时持有读锁(只要没有写锁),适用于只读操作。
  • 写锁(排他锁):当一个线程持有写锁时,其他所有线程(无论读还是写)都必须等待,适用于修改操作。

适用场景

  • 缓存系统(如 Redis 本地缓存同步)
  • 配置热更新(读配置频繁,修改配置极少)
  • 共享数据的批量读取 vs 偶发写入(如排行榜、商品库存缓存)

Java内置读写锁:ReentrantReadWriteLock(含完整案例)

Java 在 java.util.concurrent.locks 中提供了 ReentrantReadWriteLock,它是可重入的(同一个线程可以多次获取读锁或写锁),支持公平/非公平策略。

案例:线程安全的本地缓存实现

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteCache {
    private final Map<String, Object> cache = new HashMap<>();
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    // 写数据(写锁)
    public void put(String key, Object value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在写入数据...");
            // 模拟耗时写操作
            Thread.sleep(100);
            cache.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 写入完成");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            rwLock.writeLock().unlock();
        }
    }
    // 读数据(读锁)
    public Object get(String key) {
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在读取数据...");
            // 模拟耗时读操作
            Thread.sleep(50);
            return cache.get(key);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        } finally {
            rwLock.readLock().unlock();
        }
    }
    // 测试并发读写
    public static void main(String[] args) {
        ReadWriteCache myCache = new ReadWriteCache();
        // 启动一个写线程
        new Thread(() -> myCache.put("key1", "value1"), "写线程-1").start();
        // 启动多个读线程
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println("读取结果: " + myCache.get("key1"));
            }, "读线程-" + i).start();
        }
    }
}

运行效果解析

  • 写线程获取写锁时,所有读线程阻塞等待。
  • 当写锁释放后,多个读线程几乎同时获取读锁,并行读取,不再串行。
  • 这显著降低了读操作的响应时间,提高了吞吐量。

注意事项

  • 不要忘记在 finally 中释放锁,避免死锁。
  • 写锁可以降级为读锁(通过先获取写锁,再获取读锁,最后释放写锁),但读锁不能升级为写锁(否则死锁)。

手写一个简易读写锁(自定义实现)

为了加深理解,我们基于 synchronized + 等待/通知 机制实现一个简单的读写锁(非重入版本)。

public class SimpleReadWriteLock {
    private int readers = 0;       // 当前持有读锁的线程数
    private int writers = 0;       // 当前持有写锁的线程数(只能0或1)
    private int writeRequests = 0; // 等待写锁的线程数
    // 获取读锁
    public synchronized void lockRead() throws InterruptedException {
        while (writers > 0 || writeRequests > 0) {
            wait();  // 有写线程在活动或等待时,阻塞
        }
        readers++;
    }
    // 释放读锁
    public synchronized void unlockRead() {
        readers--;
        notifyAll();
    }
    // 获取写锁
    public synchronized void lockWrite() throws InterruptedException {
        writeRequests++;
        while (readers > 0 || writers > 0) {
            wait();  // 等待所有读锁和写锁释放
        }
        writeRequests--;
        writers = 1;
    }
    // 释放写锁
    public synchronized void unlockWrite() {
        writers = 0;
        notifyAll();
    }
}

缺陷

  • 不支持可重入(同线程不能重复加锁)
  • 不公平(可能有写线程一直饥饿)
  • 性能不如 ReentrantReadWriteLock(使用了全局 synchronized

价值:展示了读写锁最本质的“读共享、写互斥”逻辑,适合面试手写。


读写锁常见陷阱与性能优化

陷阱1:读锁阻塞写锁(写线程饥饿)

如果一直有读线程获取读锁,写线程可能永远拿不到写锁。解决方案

  • 使用 ReentrantReadWriteLock 的公平模式(new ReentrantReadWriteLock(true)
  • 或自定义策略:当写线程等待时,禁止新读线程进入(如上面的自定义实现中的 writeRequests 判断)

陷阱2:锁降级导致死锁

错误示例

writeLock.lock();
readLock.lock();   // 先获取读锁
writeLock.unlock(); // 再释放写锁
// 此时仍然占用读锁(降级成功)

但反过来(读锁升级为写锁)会导致死锁,因为一个线程持有读锁时,其他线程无法获取写锁,而它自己又在等待写锁。

性能优化建议

  • 如果读操作非常快(微秒级),读写锁的开销可能比直接用 synchronized 还高(锁获取/释放的CAS操作),可以用 LongAdder 或无锁结构代替。
  • 对于“写极少,读极多”的场景,可以考虑 CopyOnWriteArrayListStampedLock(乐观读锁,JDK8+)。

Q&A:高频面试题与实战解答

Q1:读写锁和互斥锁的性能差距有多大?
A:在读多写少的场景下(例如90%读,10%写),读写锁的吞吐量可能是互斥锁的3-10倍,但具体取决于并发线程数和锁的竞争程度。

Q2:ReentrantReadWriteLock 的读锁可以重入吗?
A:可以,同一线程可以多次获取读锁(需要释放相同次数),但写锁重入时,如果已经持有读锁,会死锁(不支持升级)。

Q3:如果业务要求“读不允许等待写”,怎么办?
A:可以使用 StampedLock 的乐观读模式(tryOptimisticRead),写操作不阻塞读,但读操作需要验证数据版本。

Q4:手写读写锁时,如何避免线程饥饿?
A:可以在读锁获取前检查是否有写线程在等待(如 writeRequests > 0 则阻塞),或者引入公平队列(如使用 AbstractQueuedSynchronizer 的子类)。


何时用读写锁,何时不用

场景 推荐方案
读远多于写(如缓存、配置) 使用 ReentrantReadWriteLockStampedLock
读写频率相近 普通互斥锁即可(如 ReentrantLock
读操作极快(微秒级) 优先考虑无锁方案(如 volatileAtomicReference
需要乐观读且容忍短暂不一致 StampedLock 乐观读

核心原则:不要因为 Java 提供了读写锁就盲目使用,先评估业务场景的读写比例、锁持有时间、并发量,再做出选择,如果读锁保护的关键路径很短(如几十微秒),互斥锁的简单性往往更好。

通过本文的案例代码和原理分析,你应该能熟练在 Java 项目中实现读写锁,并能应对面试中的手写锁问题,如果还有疑问,欢迎在评论区交流。

抱歉,评论功能暂时关闭!