读写锁 (ReadWriteLock)

一、什么是读写锁?

读写锁是一种特殊的锁,它允许多个线程同时 数据,但只允许一个线程数据。简单来说就是:

读写锁的原则:

  • 读读不互斥:多个线程可以同时读数据
  • 读写互斥:有线程在读时不能写,有线程在写时不能读
  • 写写互斥:同时只能有一个线程写数据

二、为什么需要读写锁?

传统锁的问题:

java 复制代码
// 使用普通锁时
synchronized void readData() {
    // 即使只是读取,也要排队等待
}

synchronized void writeData() {
    // 写入数据
}

问题:多个线程只是读取数据,也要互相等待,效率低下。

读写锁的优势:

  • 提高性能:对于读多写少的场景,允许多个线程同时读取
  • 保证数据一致性:写操作时排他,防止脏读
  • 更细粒度的控制:区分读操作和写操作

三、Java中的ReadWriteLock接口

Java提供了java.util.concurrent.locks.ReadWriteLock接口:

java 复制代码
public interface ReadWriteLock {
    Lock readLock();    // 获取读锁
    Lock writeLock();   // 获取写锁
}

主要实现类:ReentrantReadWriteLock

java 复制代码
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    
    private int value;
    
    public void read() {
        readLock.lock();
        try {
            System.out.println("读取数据: " + value);
        } finally {
            readLock.unlock();
        }
    }
    
    public void write(int newValue) {
        writeLock.lock();
        try {
            value = newValue;
            System.out.println("写入数据: " + newValue);
        } finally {
            writeLock.unlock();
        }
    }
}

四、简单易懂的例子

场景:图书馆借书

java 复制代码
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Library {
    // 图书数据
    private String bookContent = "这是一本好书...";
    
    // 读写锁
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    // 读者读书(可以多人同时读)
    public void readBook(String readerName) {
        lock.readLock().lock();  // 获取读锁
        try {
            System.out.println(readerName + " 正在读书: " + bookContent.substring(0, 10) + "...");
            Thread.sleep(1000);  // 模拟阅读时间
            System.out.println(readerName + " 读完了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();  // 释放读锁
        }
    }
    
    // 作者修改书(一次只能一个人修改)
    public void writeBook(String authorName, String newContent) {
        lock.writeLock().lock();  // 获取写锁
        try {
            System.out.println(authorName + " 开始修改书籍...");
            Thread.sleep(2000);  // 模拟写作时间
            bookContent = newContent;
            System.out.println(authorName + " 修改完成");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();  // 释放写锁
        }
    }
}

测试代码:

java 复制代码
public class TestLibrary {
    public static void main(String[] args) {
        Library library = new Library();
        
        // 创建多个读者线程
        for (int i = 1; i <= 3; i++) {
            String readerName = "读者" + i;
            new Thread(() -> library.readBook(readerName)).start();
        }
        
        // 创建一个作者线程
        new Thread(() -> library.writeBook("作者1", "新的内容...")).start();
        
        // 再创建一些读者
        for (int i = 4; i <= 5; i++) {
            String readerName = "读者" + i;
            new Thread(() -> library.readBook(readerName)).start();
        }
    }
}

五、核心特性详解

1. 锁降级(重要特性)

允许从写锁 降级为读锁,但不能升级。

java 复制代码
public void lockDowngrade() {
    writeLock.lock();  // 先获取写锁
    try {
        // 修改数据
        value = 100;
        
        // 降级为读锁(必须先获取读锁,再释放写锁)
        readLock.lock();
    } finally {
        writeLock.unlock();  // 释放写锁,降级为读锁
    }
    
    try {
        // 此时仍然持有读锁,可以安全读取
        System.out.println("读取: " + value);
    } finally {
        readLock.unlock();
    }
}

2. 公平性选择

java 复制代码
// 非公平锁(默认,吞吐量高)
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

// 公平锁(按申请顺序获得锁)
ReentrantReadWriteLock fairLock = new ReentrantReadWriteLock(true);

3. 重入性支持

  • 读锁:同一线程可多次获取读锁
  • 写锁:同一线程可多次获取写锁
  • 写线程可以获取读锁,但读线程不能获取写锁

六、使用注意事项

1. 避免锁饥饿

长时间有读锁时,写线程可能一直等待。

java 复制代码
// 解决方法:使用公平锁,但性能会下降
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);

2. 注意锁的释放

一定要在finally块中释放锁:

java 复制代码
readLock.lock();
try {
    // 业务代码
} finally {
    readLock.unlock();  // 确保锁被释放
}

3. 适用场景

  • ✅ 适合:读多写少的场景(如缓存、配置信息)
  • ❌ 不适合:写操作频繁的场景
  • ❌ 不适合:读取时间非常长的场景(会导致写线程饥饿)

七、与普通锁的对比

特性 普通锁 (synchronized) 读写锁 (ReadWriteLock)
读读并发 ❌ 不允许 ✅ 允许
读写并发 ❌ 不允许 ❌ 不允许
写写并发 ❌ 不允许 ❌ 不允许
性能 简单但效率低 复杂但效率高
适用场景 简单同步 读多写少

八、实际应用示例:缓存实现

java 复制代码
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Cache<K, V> {
    private final Map<K, V> map = new HashMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    // 获取数据(多个线程可同时读)
    public V get(K key) {
        lock.readLock().lock();
        try {
            return map.get(key);
        } finally {
            lock.readLock().unlock();
        }
    }
    
    // 放入数据(一次只能一个线程写)
    public void put(K key, V value) {
        lock.writeLock().lock();
        try {
            map.put(key, value);
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    // 清空缓存
    public void clear() {
        lock.writeLock().lock();
        try {
            map.clear();
        } finally {
            lock.writeLock().unlock();
        }
    }
}

总结

读写锁的核心思想:共享读,独占写

  • 优点:在读多写少的场景中显著提高性能
  • 缺点:实现复杂,可能引起写线程饥饿
  • 关键点
    1. 读锁是共享的,写锁是独占的
    2. 读写互斥,写写互斥,但读读不互斥
    3. 支持锁降级,不支持锁升级
    4. 使用时要确保正确释放锁

建议:如果有一个数据结构,大部分操作是读取,只有少数情况会修改,那么使用读写锁是个好选择!

相关推荐
好评1241 小时前
C/C++ 内存管理:摆脱野指针和内存泄漏
开发语言·c++·内存管理·c/c++
不会玩电脑的Xin.1 小时前
Spring框架入门:IOC与AOP实战
java·后端·spring
灰灰勇闯IT1 小时前
虚拟机性能优化实战:从基础调优到深度压榨性能
开发语言·学习·性能优化·虚拟机
霸王大陆1 小时前
《零基础学PHP:从入门到实战》教程-模块八:面向对象编程(OOP)入门-5
开发语言·笔记·php·课程设计
毕设源码-郭学长1 小时前
【开题答辩全过程】以 基于java的校园安全管理系统为例,包含答辩的问题和答案
java·开发语言
ranchor6661 小时前
pandas 模拟题
开发语言·python·pandas
xun_xin6661 小时前
如何解决Qt与OpenCV编译器不匹配问题
开发语言·qt·opencv
代码雕刻家1 小时前
C语言中fgets函数详解
c语言·开发语言
雨中飘荡的记忆1 小时前
拼团系统设计与实现
java·spring boot