Java并发编程:ReentrantReadWriteLock读写锁
前言
在Java并发编程中,锁机制是保证线程安全的重要手段。synchronized和ReentrantLock都是排他锁,同一时刻只允许一个线程访问共享资源。但在实际业务场景中,读操作往往远多于写操作,如果多个读线程之间也要互相等待,会严重影响系统性能。
ReentrantReadWriteLock(读写锁)正是为了解决这个问题而设计的。它维护了一对锁:读锁和写锁,通过读写分离的策略,大幅提升并发性能。
一、读写锁的核心特性
| 锁类型 | 特性 | 说明 |
|---|---|---|
| 读锁 | 共享锁 | 多个线程可同时持有读锁 |
| 写锁 | 排他锁 | 同一时刻只能有一个线程持有写锁 |
读写锁遵循以下基本原则:
读-读共享:多个读线程可以同时执行
写-写互斥:多个写线程不能同时执行
读-写互斥:读操作和写操作不能同时进行
二、三种场景代码实战
2.1 读读共享
读锁是共享锁,多个线程可以同时获取读锁,互不阻塞。
public class ReadReadTest { public static void main(String[] args) { MyTask myTask = new MyTask(); new Thread(() -> myTask.read(), "t1").start(); new Thread(() -> myTask.read(), "t2").start(); } static class MyTask { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void read() { try { lock.readLock().lock(); System.out.println(Thread.currentThread().getName() + " start"); Thread.sleep(10000); System.out.println(Thread.currentThread().getName() + " end"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.readLock().unlock(); } } } }运行结果:
t1 start t2 start t1 end t2 end
可以看到,t1和t2几乎同时开始,读锁不互斥。
2.2 写写互斥
写锁是排他锁,同一时刻只能有一个线程持有写锁。
public class WriteWriteTest { public static void main(String[] args) { MyTask myTask = new MyTask(); new Thread(() -> myTask.write(), "t1").start(); new Thread(() -> myTask.write(), "t2").start(); } static class MyTask { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void write() { try { lock.writeLock().lock(); System.out.println(Thread.currentThread().getName() + " start"); Thread.sleep(10000); System.out.println(Thread.currentThread().getName() + " end"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); } } } }运行结果:
t1 start t1 end t2 start t2 end
t2必须等待t1释放写锁后才能执行,写锁互斥。
2.3 读写互斥
读写操作互斥,读操作进行时写操作必须等待,反之亦然。
public class ReadWriteTest { public static void main(String[] args) { MyTask myTask = new MyTask(); Thread t1 = new Thread(() -> myTask.read(), "t1"); Thread t2 = new Thread(() -> myTask.write(), "t2"); t1.start(); // 确保t1先获取读锁 Thread.sleep(2000); t2.start(); } static class MyTask { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void read() { try { lock.readLock().lock(); System.out.println(Thread.currentThread().getName() + " start"); Thread.sleep(10000); System.out.println(Thread.currentThread().getName() + " end"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.readLock().unlock(); } } public void write() { try { lock.writeLock().lock(); System.out.println(Thread.currentThread().getName() + " start"); Thread.sleep(10000); System.out.println(Thread.currentThread().getName() + " end"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); } } } }运行结果:
t1 start t1 end t2 start t2 end
t2必须等待t1读完才能开始写,读写互斥。
三、读写锁的适用场景
读写锁特别适合读多写少的业务场景:
| 场景 | 是否适用 | 原因 |
|---|---|---|
| 缓存系统 | ✅ 非常适合 | 大量读操作,偶尔更新 |
| 配置中心 | ✅ 非常适合 | 配置读取频繁,变更极少 |
| 计数器 | ❌ 不适合 | 读写比例接近1:1 |
| 账户转账 | ❌ 不适合 | 写操作频繁,锁竞争严重 |
四、注意事项
4.1 锁降级
ReentrantReadWriteLock支持锁降级:写锁可以降级为读锁,但读锁不能升级为写锁。
// 锁降级(允许) writeLock.lock(); readLock.lock(); // 写锁持有状态下获取读锁 writeLock.unlock(); // 仍然持有读锁 // 锁升级(不允许,会死锁) readLock.lock(); writeLock.lock(); // 线程永久阻塞!5.2 公平性选择
与ReentrantLock一样,读写锁也支持公平/非公平模式:
非公平模式(默认):吞吐量更高,但可能造成写线程饥饿
公平模式:线程按请求顺序获取锁,避免饥饿
// 公平读写锁 ReentrantReadWriteLock fairLock = new ReentrantReadWriteLock(true);5.3 重入特性
读写锁支持锁重入,但需要注意:
读线程可以重复获取读锁
写线程可以重复获取写锁,也可以获取读锁(锁降级)
六、总结
| 对比项 | ReentrantLock | ReentrantReadWriteLock |
|---|---|---|
| 锁类型 | 排他锁 | 读写分离 |
| 读并发 | 不并发 | 并发 |
| 写并发 | 不并发 | 不并发 |
| 适用场景 | 读写均衡 | 读多写少 |
最佳实践:在读操作远多于写操作的场景下,优先考虑使用ReentrantReadWriteLock,它可以显著提升系统吞吐量,让并发性能得到质的飞跃。
