引言
在高并发环境中,读写锁(Read-Write Lock)是一种非常重要的同步工具。它们允许多个线程同时进行读操作,但在有写操作时确保独占访问。Java 提供了 ReentrantReadWriteLock
和 StampedLock
两种读写锁实现,分别适用于不同的场景。本文将详细解析这两种读写锁的内部机制。
一、ReentrantReadWriteLock 源码解析
1.1 结构概述
ReentrantReadWriteLock
是一个可重入的读写锁,它维护了一对相关联的锁:一个用于只读操作(读锁),另一个用于写入操作(写锁)。读锁是共享锁,允许多个线程同时持有;而写锁是独占锁,一次只能有一个线程持有。此外,ReentrantReadWriteLock
支持公平性和非公平性选择,并且具备锁降级功能。
1.2 状态表示
为了管理读锁和写锁的状态,ReentrantReadWriteLock
使用了一个整数 state
来存储信息。这个整数被分为两部分:
- 高 16 位用来表示读状态。
- 低 16 位用来表示写状态。
通过位运算可以快速获取或设置读写状态。例如,sharedCount(int c)
方法用于获取当前持有的读锁数量,而 exclusiveCount(int c)
方法则用于获取写锁的数量。
java">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; }
1.3 锁的获取与释放
-
读锁获取:当尝试获取读锁时,首先检查是否有其他线程正在写入。如果没有,则增加读计数器并返回成功。如果有写请求但调用线程已经持有了写锁,则也可以获取读锁。
-
写锁获取:获取写锁时会检查是否已经有其他线程持有读锁或写锁。如果是非公平模式,即使队列中有等待者也会尝试直接获取锁;如果是公平模式,则必须按照顺序排队。
-
锁的释放:无论是读锁还是写锁,在释放时都会减少相应的计数器。当所有类型的锁都被完全释放后,才会唤醒下一个等待的线程。
1.4 锁降级
锁降级指的是从写锁转换为读锁的过程。如果一个线程先获得了写锁,然后在同一时间内又获得了读锁,最后再释放写锁,那么此时该线程就只剩下了读锁。这有助于提高性能,因为在某些情况下,不需要立即释放写锁就可以继续执行后续的读操作。
二、StampedLock 源码解析
2.1 特点介绍
StampedLock
是 Java 8 引入的一种新的读写锁实现,它的主要特点是提供了乐观读模式。这意味着读取数据时不加锁,只有在检测到数据可能已经被修改的情况下才会上悲观读锁。这种方式极大地提高了读操作的吞吐量,减少了线程饥饿的问题。
2.2 内部机制
StampedLock
不像 ReentrantReadWriteLock
那样依赖于 AQS 框架,而是采用了一种更轻量级的设计。它使用“邮戳”(stamp)来跟踪锁的状态变化。每次获取锁时都会返回一个唯一的 stamp 值,之后可以通过 validate()
方法验证该 stamp 是否仍然有效。
-
写锁:类似于
ReentrantReadWriteLock
的写锁行为,保证同一时刻只有一个线程能够进行写操作。 -
悲观读锁:允许多个线程同时持有读锁,但与写锁互斥。
-
乐观读:这是
StampedLock
的核心特性之一。在线程试图读取数据之前,它会尝试以乐观的方式获取一个 stamp。如果在此期间没有其他线程修改数据,则可以直接使用读取的数据;否则需要重新上悲观读锁并重新读取数据。
2.3 示例代码
下面是一个简单的例子展示了如何使用 StampedLock
进行乐观读:
java">public class Point {private final StampedLock stampedLock = new StampedLock();private double x, y;public void move(double deltaX, double deltaY) {long stamp = stampedLock.writeLock();try {x += deltaX;y += deltaY;} finally {stampedLock.unlockWrite(stamp);}}public double distanceFromOrigin() {long stamp = stampedLock.tryOptimisticRead();double currentX, currentY;currentX = x;if (!stampedLock.validate(stamp)) {stamp = stampedLock.readLock();try {currentX = x;currentY = y;} finally {stampedLock.unlockRead(stamp);}} else {currentY = y;}return Math.sqrt(currentX * currentX + currentY * currentY);}
}
总结
通过对 ReentrantReadWriteLock
和 StampedLock
的源码解析,我们可以看到两者虽然都实现了读写锁的功能,但在设计哲学和技术细节上有着显著的区别。前者更适合那些需要严格控制线程安全性的场合,而后者则更加关注高性能和高吞吐量的应用场景。理解这些底层原理不仅有助于我们编写高效的并发程序,也能帮助我们在实际开发中做出更好的技术选型决策。