读写锁,顾名思义。这是一种对于两种不同行为的同步器工具,而读写是两种互斥的行为(写的时候,不能读。读的时候不能写),因此ReentrantReadWriteLock锁也具有此特性。
1 用写公司简章过程理解读写锁
想象一下,你正在写一份公司内部管理的简章,而笔只有一支(写锁),所以只有当你拿到这只笔(获取写锁)后才可以写内容。写完后(释放锁),简章就可以张贴出去让同事去阅读了(同时阅读:读锁)。但是这个时候你发现简章上写的有些问题,需要重新修改一下。不过你需要等你这些同事看完全部离开之后(释放读锁),你才可以拿着笔(写锁)去修改简章。修改完成后,为了保证这次写的没有问题,你决定自己先检查一下(持有写锁,再持有读锁),OK后你放下笔(释放写锁,这个过程就交锁降级:写锁->读锁),然后又把它张贴到布告栏中,让同事们去观看。
2
源码解析
这里只要来学习其设计,对于公平锁非公平锁,以及可重入的概念不再赘述,不太清楚的可参考AQS-用配钥匙和保险箱理解可重入锁(ReentrantLock)这篇文章的解析。读写锁也是通过继承AQS同步器来实现其功能的,它同时使用了排他锁(写锁)和共享锁(读锁),现在看它是怎么实现将读锁和写锁关联起来的。
构造函数:
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this); // sync = lock.sync;
writerLock = new WriteLock(this); // sync = lock.sync;
}
读写锁,最终使用的都是这里构造的sync对象,根据fair参数生成公平锁或非公平锁。接下来我们来看一下读锁的获取/释放,以及写锁的获取/释放。
// 读锁
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
// 写锁
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
可以看到的是,读锁使用的是共享锁逻辑,而写锁用的是排他锁的逻辑,同时要注意其可重入的。这些都很简单的,我们前面的文章都有讲述到。这里的难点是读锁和写锁到底是这么关联的?解析源码前,我们先来看一下Sync这个class的成员信息,我们就能明白了ReentrantReadWriteLock是怎么实现的了:
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; }
设计者将int32分为两部分:前16位(存储读锁信息),后16位(存储写锁信息)。我们再根据其提供的sharedCount,exclusiveCount 方法名就可以明白,ReentrantReadWriteLock读写锁就是获取读锁数量和写锁数量来实现关联,从而实现互斥功能。现在我们来看一下实际代码:
读锁:
// 读锁lock sync.acquireShared(1)最终调用tryReleaseShared
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 && ## 1
getExclusiveOwnerThread() != current) // 判断是否可以锁降级(持有写锁的,获取读锁)
return -1; // 失败。因为已经有人持有写锁了
int r = sharedCount(c); // 读锁的数量
if (!readerShouldBlock() && ## 2
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) { ## 3
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;
}
return fullTryAcquireShared(current); // 完整版,用了for循环,代码类似不赘述。
}
说明下代码:
1:if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) 代码实现的是读写锁的互斥以及写锁的降级。解释一下:写锁数量不为0,当前线程不是索取写锁的线程。只有if为false才能获取读锁,有以下两种情情况:a当前没有线程写锁; b.是当前线程获取写锁。
2 : 这里的逻辑就是获取读锁,使用了CAS。
3 : 这里说明一下 :firstReader和cachedHoldCounter记录第一个和最后一个线程是为了优化获取锁的效率。firstReader(只有一个线程时重复获取锁)。cachedHoldCounter(最后一个获取锁的线程重复获取锁)。因为这两种情况最有可能发生。
写锁:
// 写锁lock sync.acquireShared(1)最终调用tryReleaseShared
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); // 状态值
int w = exclusiveCount(c); // 写锁数量
if (c != 0) { ## 1
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT) // 最大重入数量
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() || // 获取读锁锁逻辑
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current); // 设置当前线程,便于重入
return true;
}
1: 状态值state不为0,意味着有线程持有锁(写锁,读锁)。 if (w == 0 || current != getExclusiveOwnerThread()) ,写锁数量为0,那么现在还有读锁未释放,获取失败。如果写锁数量不为0,但不是当前线程则不可重入。
对于读写锁的释放比较简单,这里就不再解析了。同时需要说一声的是关于java AQS这块源码学习就先到这了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。