1. 读写锁概念
1.1 为什么需要读写锁
前面我们介绍了管程原语在 Java 语言中的实现,理论上用这个同步原语可以解决所有的并发问题。那 Java SDK 并发包里为什么还有很多其他的工具类呢?原因很简单:分场景优化性能,提升易用性。
在并发编程中解决线程安全的问题,通常使用的都是java提供的关键字synchronized或者重入锁ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。
但是在大多数场景下,大部分时间都是读取共享资源,对共享资源的写操作很少。然而读服务不存在数据竞争问题,如果一个线程在读时禁止其他线程读势必会导致性能降低。
针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。读写锁允许共享资源在同一时刻可以被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
1.2 读写锁原则
读写锁,并不是 Java 语言特有的,而是一个广为使用的通用技术,所有的读写锁都遵守以下三条基本原则:
- 允许多个线程同时读共享变量;
- 只允许一个线程写共享变量;
- 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
读写锁与互斥锁的一个重要区别就是读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。
public class TestReentrantReadWriteLock {
private static int data = 0;
private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void put(int data) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 线程正在写...");
try {
Thread.sleep((long) Math.random() * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TestReentrantReadWriteLock.data = data;
System.out.println(Thread.currentThread().getName() + " 写完数据" + data + "...");
} finally {
lock.writeLock().unlock();
}
}
public static void get() {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 线程正在读...");
try {
Thread.sleep((long) Math.random() * 2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 线程读取数据完成" + data + "...");
} finally {
lock.readLock().unlock();
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
get();
}, "读线程" + i).start();
}
for (int i = 0; i < 3; i++) {
new Thread(() -> {
put(new Random().nextInt(10000));
}, "写线程" + i).start();
}
}
}
#代码运行如下:
读线程0 线程正在读...
读线程1 线程正在读...
读线程2 线程正在读...
读线程0 线程读取数据完成0...
读线程2 线程读取数据完成0...
读线程1 线程读取数据完成0...
写线程0 线程正在写...
写线程0 写完数据7399...
写线程1 线程正在写...
写线程1 写完数据1388...
写线程2 线程正在写...
写线程2 写完数据9762...
从结果可以看出,读锁是共享的,读锁的三个线程是同时读取共享数据data的;写锁是互斥的,写锁的三个线程是依次写共享数据data的。
1.3 读写锁的应用
下面我们就实践起来,用 ReadWriteLock 快速实现一个通用的缓存工具类,如果你曾经使用过缓存的话,你应该知道使用缓存首先要解决缓存数据的初始化问题。缓存数据的初始化,可以采用一次性加载的方式,也可以使用按需加载的方式。
如果源头数据的数据量不大,就可以采用一次性加载的方式,这种方式最简单,只需在应用启动的时候把源头数据查询出来,依次调用类似下面示例代码中的 put() 方法就可以了。如果源头数据量非常大,那么就需要按需加载了,按需加载也叫懒加载,指的是只有当应用查询缓存,并且数据不在缓存里的时候,才触发加载源头相关数据进缓存的操作。
public class Cache<K, V> {
final Map<K, V> map = new HashMap<>();
final ReadWriteLock rwl = new ReentrantReadWriteLock();
final Lock readLock = rwl.readLock();
final Lock writeLock = rwl.writeLock();
public V get(K key) {
V value = null;
readLock.lock(); //①
try {
value = map.get(key); //②
} finally {
readLock.unlock(); //③
}
if (value != null) { //④
return value;
}
// 缓存中不存在,查询数据库
writeLock.lock(); // ⑤
try {
//再次验证, 其他线程可能已经查询过数据库
value = map.get(key); //⑥
if (value == null) { //⑦
// query DB
// value = "query DB";
map.put(key, value);
}
} finally {
writeLock.unlock();
}
return value;
}
public void put(K key, V value) {
writeLock.lock();
try {
map.put(key, value);
} finally {
writeLock.unlock();
}
}
}
需要注意的是,在获取写锁之后,我们并没有直接去查询数据库,而是在代码⑥⑦处,重新验证了一次缓存中是否存在,再次验证如果还是不存在,我们才去查询数据库并更新本地缓存。为什么我们要再次验证呢?
原因是在高并发的场景下,有可能会有多线程竞争写锁。假设缓存是空的,没有缓存任何东西,如果此时有三个线程 T1、T2 和 T3 同时调用 get() 方法,并且参数 key 也是相同的。那么它们会同时执行到代码⑤处,但此时只有一个线程能够获得写锁,假设是线程 T1,线程 T1 获取写锁之后查询数据库并更新缓存,最终释放写锁。此时线程 T2 和 T3 会再有一个线程能够获取写锁,假设是 T2,如果不采用再次验证的方式,此时 T2 会再次查询数据库。T2 释放写锁之后,T3 也会再次查询一次数据库。而实际上线程 T1 已经把缓存的值设置好了,T2、T3 完全没有必要再次查询数据库。所以,再次验证的方式,能够避免高并发场景下重复查询数据的问题。
1.4 读写锁的降级
上面按需加载的示例代码中,在①处获取读锁,在③处释放读锁,那是否可以在②处的下面增加验证缓存并更新缓存的逻辑呢?详细的代码如下:
//读缓存
r.lock(); //①
try {
v = m.get(key); //②
if (v == null) {
w.lock();
try {
//再次验证并更新缓存
//省略详细代码
} finally {
w.unlock();
}
}
} finally {
r.unlock(); //③
}
这样看上去好像是没有问题的,先是获取读锁,然后再升级为写锁,对此还有个专业的名字,叫锁的升级。可惜 ReadWriteLock 并不支持这种升级。在上面的代码示例中,读锁还没有释放,此时获取写锁,会导致写锁永久等待,最终导致相关线程都被阻塞,永远也没有机会被唤醒。锁的升级是不允许的,这个你一定要注意。
不过,虽然锁的升级是不允许的,但是锁的降级却是允许的。以下代码来源自 ReentrantReadWriteLock 的官方示例,略做了改动。你会发现在代码①处,获取读锁的时候线程还是持有写锁的,这种锁的降级是支持的。
class CachedData {
Object data;
volatile boolean cacheValid;
final ReadWriteLock rwl =
new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
//写锁
final Lock w = rwl.writeLock();
void processCachedData() {
// 获取读锁
r.lock();
if (!cacheValid) {
// 释放读锁,因为不允许读锁的升级
r.unlock();
// 获取写锁
w.lock();
try {
// 再次检查状态
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 释放写锁前,降级为读锁
// 降级是可以的
r.lock(); ①
} finally {
// 释放写锁
w.unlock();
}
}
// 此处仍然持有读锁
try {
use(data);
} finally {
r.unlock();
}
}
}
2. 源码分析
2.1 类结构
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
// 属性
/** Inner class providing readlock 读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock 写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics 锁的主体 AQS */
final Sync sync;
// 内部类
/**
* Synchronization implementation for ReentrantReadWriteLock.
* Subclassed into fair and nonfair versions.
*/
abstract static class Sync extends AbstractQueuedSynchronizer{}
/**
* Nonfair version of Sync
*/
static final class NonfairSync extends Sync{}
/**
* Fair version of Sync
*/
static final class FairSync extends Sync{}
/**
* The lock returned by method {@link ReentrantReadWriteLock#readLock}.
*/
public static class ReadLock implements Lock, java.io.Serializable{}
/**
* The lock returned by method {@link ReentrantReadWriteLock#writeLock}.
*/
public static class WriteLock implements Lock, java.io.Serializable{}
}
// 构造方法
/**
* Creates a new {@code ReentrantReadWriteLock} with
* default (nonfair) ordering properties.
*/
public ReentrantReadWriteLock() {
this(false);
}
/**
* Creates a new {@code ReentrantReadWriteLock} with
* the given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
ReentrantReadWriteLock与ReentrantLock一样,其锁主体依然是Sync,读写锁其实就是两个属性:readerLock、writerLock。
一个ReentrantReadWriteLock对象都对应着读锁和写锁两个锁,而这两个锁是通过同一个sync(AQS)实现的。
2.2 记录读写锁的状态
我们知道AQS.state使用来表示同步状态的。ReentrantLock中,state=0表示没有线程占用锁,state>0时state表示线程的重入次数。但是读写锁ReentrantReadWriteLock内部维护着两个锁,需要用state这一个变量维护多种状态,应该怎么办呢?
读写锁采用“按位切割使用”的方式,将state这个int变量分为高16位和低16位,高16位记录读锁状态,低16位记录写锁状态,并通过位运算来快速获取当前的读写锁状态。
/**
* Synchronization implementation for ReentrantReadWriteLock.
* Subclassed into fair and nonfair versions.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
/*
* Read vs write count extraction constants and functions.
* Lock state is logically divided into two unsigned shorts:
* The lower one representing the exclusive (writer) lock hold count,
*
* 将state这个int变量分为高16位和低16位,
* 高16位记录读锁状态,低16位记录写锁状态
*
* and the upper the shared (reader) hold count.
*/
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;
/**
* Returns the number of shared holds represented in count
* 获取读锁的状态,读锁的获取次数(包括重入)
* c无符号补0右移16位,获得高16位
*/
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/**
* Returns the number of exclusive holds represented in count
* 获取写锁的状态,写锁的重入次数
* c & 0x0000FFFF,将高16位全部抹去,获得低16位
*/
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK;
}
2.3 记录获取锁的线程
线程获取写锁后,和重入锁一样,将AQS.exclusiveOwnerThread置为当前线程。但是读锁是共享的,可以多个线程同时获取读锁,那么如何记录获取读锁的多个线程以及每个线程的重入情况呢?
sycn中提供了一个HoldCounter类,类似计数器,用于记录一个线程读锁的重入次数。将HoldCounter通过ThreadLocal与线程绑定。
/**
* Synchronization implementation for ReentrantReadWriteLock.
* Subclassed into fair and nonfair versions.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
/**
* A counter for per-thread read hold counts.
* Maintained as a ThreadLocal; cached in cachedHoldCounter
* 这个嵌套类的实例用来记录每个线程持有的读锁数量(读锁重入)
*/
static final class HoldCounter {
int count = 0; // 读锁重入次数
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread()); // 线程ID
}
/**
* ThreadLocal subclass. Easiest to explicitly define for sake
* of deserialization mechanics. ThreadLocal 的子类
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
/**
* The number of reentrant read locks held by current thread.
* Initialized only in constructor and readObject.
* Removed whenever a thread's read hold count drops to 0.
*
* 组合使用上面两个类,用一个 ThreadLocal 来记录当前线程持有的读锁数量
*/
private transient ThreadLocalHoldCounter readHolds;
/**
* The hold count of the last thread to successfully acquire
* readLock. This saves ThreadLocal lookup in the common case
* where the next thread to release is the last one to
* acquire. This is non-volatile since it is just used
* as a heuristic, and would be great for threads to cache.
*
* 记录"最后一个获取读锁的线程"的读锁重入次数,用于缓存提高性能
*
* <p>Can outlive the Thread for which it is caching the read
* hold count, but avoids garbage retention by not retaining a
* reference to the Thread.
*
* <p>Accessed via a benign data race; relies on the memory
* model's final field and out-of-thin-air guarantees.
*/
private transient HoldCounter cachedHoldCounter;
/**
* firstReader is the first thread to have acquired the read lock.
* firstReaderHoldCount is firstReader's hold count.
*
* <p>More precisely, firstReader is the unique thread that last
* changed the shared count from 0 to 1, and has not released the
* read lock since then; null if there is no such thread.
*
* <p>Cannot cause garbage retention unless the thread terminated
* without relinquishing its read locks, since tryReleaseShared
* sets it to null.
*
* <p>Accessed via a benign data race; relies on the memory
* model's out-of-thin-air guarantees for references.
*
* <p>This allows tracking of read holds for uncontended read
* locks to be very cheap.
*/
// 第一个获取读锁的线程(并且其未释放读锁)
private transient Thread firstReader = null;
// 第一个获取读锁的线程重入的读锁数量
private transient int firstReaderHoldCount;
}
属性cachedHoldCounter、firstReader、firstReaderHoldCount都是为了提高性能,线程与HoldCounter的存储结构如下图:
2.4 读锁获取
查看使用示例中 rwl.readLock().lock() 的实现:
/**
* Acquires the read lock.
*
* <p>Acquires the read lock if the write lock is not held by
* another thread and returns immediately.
*
* rwl.readLock().lock()-->ReadLock.lock()
*
* <p>If the write lock is held by another thread then
* the current thread becomes disabled for thread scheduling
* purposes and lies dormant until the read lock has been acquired.
*/
public void lock() {
sync.acquireShared(1);
}
/**
* Acquires in shared mode, ignoring interrupts. Implemented by
* first invoking at least once {@link #tryAcquireShared},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquireShared} until success.
*
* ReadLock.lock()-->AQS.acquireShared(int)
* sync 重写了 tryAcquireShared() 方法
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquireShared} but is otherwise uninterpreted
* and can represent anything you like.
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
2.4.1 tryAcquireShared 尝试获取读锁
tryAcquireShared() :尝试获取读锁,获取到锁返回 1,获取不到返回 -1,首先来分析一下可以获取读锁的条件:
- 当前锁的状态:读锁写锁都没有被占;只有读锁被占用;写锁被自己线程占用。简单总结就是:只有在其它线程持有写锁时,不能获取读锁,其它情况都可以去获取;
- AQS队列中的情况,如果是公平锁,同步队列中有线程等锁时,当前线程是不可以先获取锁的,必须到队列中排队;
- 读锁的标志位只有16位,最多只能有2^16-1个线程获取读锁或重入。
/**
* 尝试获取读锁,获取到锁返回1,获取不到返回-1
*/
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 && // 写锁被占用
getExclusiveOwnerThread() != current) // 持有写锁的不是当前线程
return -1;
int r = sharedCount(c);
// 检查AQS队列中的情况,看是当前线程是否可以获取读锁,下文有解释
if (!readerShouldBlock() &&
r < MAX_COUNT && // 读锁的标志位只有16位,最多之能有2^16-1个线程获取读锁或重入
// 在state的第17位加1,也就是将读锁标志位加1
compareAndSetState(c, c + SHARED_UNIT)) {
/*
* 到这里已经获取到读锁了
* 以下是修改记录获取读锁的线程和重入次数,
* 以及缓存firstReader和cachedHoldCounter
*/
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
/*
* 到这里
* 没有获取到读锁,因为上面代码获取到读锁的话已经在上一个if里返回1了
* 锁的状态是满足获取读锁的,因为不满足的上面返回-1了
* 所以没有获取到读锁的原因:AQS队列不满足获取读锁条件,
* 或者CAS失败,或者16位标志位满了
* 像CAS失败这种原因,是一定要再尝试获取的,所以这里再次尝试获取读锁,
* fullTryAcquireShared()方法下文有详细讲解
*/
return fullTryAcquireShared(current);
}
2.4.1.1 readerShouldBlock
readerShouldBlock():检查AQS队列中的情况,看是当前线程是否可以获取读锁,返回true表示当前不能获取读锁。分别看下公平锁和非公平锁的实现:
公平锁FairSync:
对于公平锁来说,如果队列中还有线程在等锁,就不允许新来的线程获得锁,必须进入队列排队。
hasQueuedPredecessors() 方法在重入锁的文章中分析过,判断同步队列中是否还有等锁的线程,如果有其他线程等锁,返回true当前线程不能获取读锁。
/**
* Fair version of Sync
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
非公平锁NonfairSync:
对于非公平锁来说,原本是不需要关心队列中的情况,有机会直接尝试抢锁就好了,这里问什么会限制获取锁呢?
这里给写锁定义了更高的优先级,如果队列中第一个等锁的线程请求的是写锁,那么当前线程就不能跟那个马上就要获取写锁的线程抢,这样做很好的避免了写锁饥饿。
/**
* Nonfair version of Sync
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*
* 队列中第一个等锁的线程请求的是写锁时,返回true,当前线程不能获取读锁
*/
return apparentlyFirstQueuedIsExclusive();
}
}
/**
* Returns {@code true} if the apparent first queued thread, if one
* exists, is waiting in exclusive mode. If this method returns
* {@code true}, and the current thread is attempting to acquire in
* shared mode (that is, this method is invoked from {@link
* #tryAcquireShared}) then it is guaranteed that the current thread
* is not the first queued thread. Used only as a heuristic in
* ReentrantReadWriteLock.
* AbstractQueuedSynchronizer 类中方法
* 返回true-队列中第一个等锁的线程请求的是写锁
*
*/
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() && // head后继节点线程请求写锁
s.thread != null;
}
2.4.1.2 fullTryAcquireShared
tryAcquireShared() 方法中因为 CAS 抢锁失败等原因没有获取到读锁的,fullTryAcquireShared() 再次尝试获取读锁。此外,fullTryAcquireShared() 还处理了读锁重入的情况。
/**
* 再次尝试获取读锁
*/
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {// 注意这里是循环
int c = getState();
if (exclusiveCount(c) != 0) {
// 仍然是先检查锁状态:在其它线程持有写锁时,不能获取读锁,返回-1
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
/*
* exclusiveCount(c) == 0 写锁没有被占用
* readerShouldBlock() == true,AQS同步队列中的线程在等锁,
* 当前线程不能抢读锁
* 既然当前线程不能抢读锁,为什么没有直接返回呢?
* 因为这里还有一种情况是可以获取读锁的,那就是读锁重入。
* 以下代码就是检查如果不是重入的话,return -1,不能继续往下获取锁。
*/
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// CAS修改读锁标志位,修改成功表示获取到读锁;
// CAS失败,则进入下一次for循环继续CAS抢锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
/*
* 到这里已经获取到读锁了
* 以下是修改记录获取读锁的线程和重入次数,
* 以及缓存firstReader和cachedHoldCounter
*/
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
2.4.1.3 doAcquireShared
再回到最开始的 acquireShared(),tryAcquireShared() 抢锁成功,直接返回,执行同步代码;如果 tryAcquireShared() 抢锁失败,调用 doAcquireShared()。
doAcquireShared() 应该比较熟悉了吧,类似 AQS 那篇中分析过 acquireQueued():
- 将当前线程构成节点 node;
- 如果 node 是 head 的后继节点就可以继续尝试抢锁;
- 如果 node 不是 head 的后继节点,将 node 加入队列的队尾,并将当前线程阻塞,等待 node 的前节点获取、释放锁之后唤醒 node 再次抢锁;
- node 抢到读锁之后执行 setHeadAndPropagate() 方法,setHeadAndPropagate() 是获取读锁的特殊之处,
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
// 把当前线程构造成节点,Node.SHARED表示共享锁
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {// 前驱节点是head,node才能去抢锁
int r = tryAcquireShared(arg);// 抢锁,上文分析了
if (r >= 0) {// r>0表示抢锁成功
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 判断node前驱节点状态,将当前线程阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
2.4.1.4 setHeadAndPropagate
试想一种情况:当线程1持有写锁时,线程2、线程3、线程4、线程5…来获取读锁是获取不到的,只能排进同步队列。当线程1释放写锁时,唤醒线程2来获取锁。因为读锁是共享锁,当线程2获取到读锁时,线程3也应该被唤醒来获取读锁。
setHeadAndPropagate()方法就是在一个线程获取读锁之后,唤醒它之后排队获取读锁的线程的。该方法可以保证线程2获取读锁后,唤醒线程3获取读锁,线程3获取读锁后,唤醒线程4获取读锁,直到遇到后继节点是要获取写锁时才结束。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);// 因为node获取到锁了,所以设置node为head
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())// node后继节点线程要获取读锁,此时node就是head
doReleaseShared();// 唤醒head后继节点(也就是node.next)获取锁
}
}
2.5 读锁释放
理解了上文读锁的获取过程,读锁的释放过程不看源码也应该可以分析出来:
- 处理 firstReader、cachedHoldCounter、readHolds 获取读锁线程及读锁重入次数;
- 修改读锁标志位 state 的高16位;
- 释放读锁之后,如果队列中还有线程等锁,唤醒同步队列 head 后继节点等待写锁的线程。这里为什么是写锁?因为线程持有读锁时会把它之后要获取读锁的线程全部唤醒直到遇到写锁。
/**
* rwl.readLock().unlock()-->ReadLock.unlock()
*/
public void unlock() {
sync.releaseShared(1);
}
/**
* sync.releaseShared(1)-->AQS.releaseShared(int)
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {// 当前线程释放读锁,下文介绍
/*
* 到这里,已经没有任何线程占用锁,调用 doReleaseShared()
* 唤醒之后获取写锁的线程
* 如果同步队列中还有线程在排队,head后继节点的线程一定是要获取写锁,
* 因为线程持有读锁时会把它之后要获取读锁的线程全部唤醒
*/
doReleaseShared();// 唤醒head后继节点获取锁
return true;
}
return false;
}
/**
* 释放读锁
* 当前线程释放读锁之后,没有线程占用锁,返回true
*/
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 处理firstReader、cachedHoldCounter、readHolds
// 获取读锁线程及读锁重入次数
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;// state第17位-1,也就是读锁状态标志位-1
// CAS设置state,CAS失败自旋进入下一次for循环
if (compareAndSetState(c, nextc))
return nextc == 0;// state=0表示没有线程占用锁,返回true
}
}
2.6 写锁获取
上文例子中 rwl.writeLock().lock() 的调用:
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 写锁实现了获取锁的方法,下文详细讲解
// 获取锁失败进入同步队列,等待被唤醒,AQS一文中重点讲过
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
先分析一下可以获取写锁的条件:
- 当前锁的状态:没有线程占用锁(读写锁都没被占用);线程占用写锁时,线程再次来获取写锁,也就是重入;
- AQS队列中的情况,如果是公平锁,同步队列中有线程等锁时,当前线程是不可以先获取锁的,必须到队列中排队;
- 写锁的标志位只有16位,最多重入2^16-1次。
/**
* ReentrantReadWriteLock.Sync.tryAcquire(int)
*/
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);// 写锁标志位
// 进到这个if里,c!=0表示有线程占用锁
// 当有线程占用锁时,只有一种情况是可以获取写锁的,那就是写锁重入
if (c != 0) {
/*
* 两种情况返回false
* 1.(c != 0 & w == 0)
* c!=0表示标志位!=0,w==0表示写锁标志位==0,
* 总的标志位不为0而写锁标志位(低16位)为0,只能是读锁标志位(高16位)不为0
* 也就是有线程占用读锁,此时不能获取写锁,返回false
*
* 2.(c != 0 & w != 0 & current != getExclusiveOwnerThread())
* c != 0 & w != 0 表示写锁标志位不为0,有线程占用写锁
* current != getExclusiveOwnerThread() 占用写锁的线程不是当前线程
* 不能获取写锁,返回false
*/
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 重入次数不能超过2^16-1
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
/*
* 修改标志位
* 这里修改标志位为什么没有用CAS原子操作呢?
* 因为到这里肯定是写锁重入了,写锁是独占锁,不会有其他线程来捣乱。
*/
setState(c + acquires);
return true;
}
/*
* 到这里表示锁是没有被线程占用的,因为锁被线程占用的情况在上个if里处理并返回了
* 所以这里直接检查AQS队列情况,没问题的话CAS修改标志位获取锁
*/
// 检查AQS队列中的情况,看是当前线程是否可以获取写锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires)) // 修改写锁标志位
return false;
// 获取写锁成功,将AQS.exclusiveOwnerThread置为当前线程
setExclusiveOwnerThread(current);
return true;
}
2.6.1 writerShouldBlock
writerShouldBlock():检查AQS队列中的情况,看是当前线程是否可以获取写锁,返回false表示可以获取写锁。
对于公平锁来说,如果队列中还有线程在等锁,就不允许新来的线程获得锁,必须进入队列排队。hasQueuedPredecessors()方法在重入锁的文章中分析过,判断同步队列中是否还有等锁的线程,如果有其他线程等锁,返回true当前线程不能获取读锁。
// 公平锁
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
对于非公平锁来说,不需要关心队列中的情况,有机会直接尝试抢锁就好了,所以直接返回false。
// 非公平锁
final boolean writerShouldBlock() {
return false;
}
2.7 写锁释放
写锁释放比较简单,跟之前的重入锁释放基本类似,看下源码:
public void unlock() {
sync.release(1);
}
/**
* 释放写锁,如果释放之后没有线程占用写锁,唤醒队列中的线程来获取锁
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);// 唤醒head的后继节点去获取锁
return true;
}
return false;
}
/**
* 释放写锁,修改写锁标志位和exclusiveOwnerThread
* 如果这个写锁释放之后,没有线程占用写锁了,返回true
*/
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
2.8 锁降级
2.8.1 锁降级
读写锁支持锁降级。锁降级就是写锁是可以降级为读锁的,但是需要遵循获取写锁、获取读锁、释放写锁的次序。
为什么要支持锁降级?
支持降级锁的情况:线程A 持有写锁时,线程A要读取共享数据,线程A 直接获取读锁读取数据就好了。
如果不支持锁降级会怎么样?
线程A 持有写锁时,线程A 要读取共享数据,但是线程A 不能获取读锁,只能等待释放写锁。
当线程A 释放写锁之后,线程A 获取读锁要和其他线程抢锁,如果另一个线程B 抢到了写锁,对数据进行了修改,那么线程B 释放写锁之后,线程A 才能获取读锁。线程B 获取到读锁之后读取的数据就不是线程A 修改的数据了,也就是脏数据。
源码中体现锁降级?
tryAcquireShared() 方法中,当前线程占用写锁时是可以获取读锁的,如下:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
/*
* 根据锁的状态判断可以获取读锁的情况:
* 1. 读锁写锁都没有被占用
* 2. 只有读锁被占用
* 3. 写锁被自己线程占用
* 总结一下,只有在其它线程持有写锁时,不能获取读锁,其它情况都可以去获取。
*/
if (exclusiveCount(c) != 0 && // 写锁被占用
getExclusiveOwnerThread() != current) // 持有写锁的不是当前线程
return -1;
...
2.8.2 锁升级
持有写锁的线程,去获取读锁的过程称为锁降级;持有读锁的线程,在没释放的情况下不能去获取写锁的过程称为锁升级。
读写锁是不支持锁升级的。获取写锁的tryAcquire()方法:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
/*
* (c != 0 & w == 0)时返回false,不能获取写锁
* c != 0 表示state不是0
* w == 0 表示写锁标志位state的低16位为0
* 所以state的高16位不为0,也就是有线程占有读锁
* 也就是说只要有线程占有读锁返回false,不能获取写锁,
* 当然线程自己持有读锁时也就不能获取写锁了
*/
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
...
3. 总结
大多数业务场景,都是读多写少的,采用互斥锁性能较差,所以提供了读写锁。读写锁允许共享资源在同一时刻可以被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
一个 ReentrantReadWriteLock 对象都对应着读锁和写锁两个锁,而这两个锁是通过同一个 sync(AQS)实现的。
读写锁采用“按位切割使用”的方式,将 state 这个 int 变量分为高 16位和低 16位,高 16位记录读锁状态,低 16位记录写锁状态。
读锁获取时,需要判断当时的写锁没有被其他线程占用即可,锁处于的其他状态都可以获取读锁。
对于写锁,可以获取写锁的情况只有两种:读锁和写锁都没有线程占用;当前线程占用写锁,也就写锁重入。
读写锁支持锁降级,不支持锁升级。锁降级就是写锁是可以降级为读锁的,但是需要遵循获取写锁、获取读锁、释放写锁的次序。
读写锁多用于解决读多写少的问题,最典型的就是缓存问题。