ReentrantReadWriteLock可重入读写锁,同样是基于AQS实现的。与ReentrantLock区别就是读写分离,将锁的粒度都将细化,提升性能。在共享数据读写操作中,读操作远远超过写操作的次数,那么可以理解为共享数据在大部分时间是不变的。synchronized和ReentrantLock作为互斥锁,用于这种场景明显会降低系统的性能。因此,读写分离的重入锁ReentrantReadWriteLock就出现了。
ReentrantLock特点是:读-读并行、读-写互斥、写-写互斥。存在对共享数据修改操作时,自然需要有互斥锁的特点,但是在不需要修改数据时,可以认为是无锁的并行操作。
来个简单的例子:
public class TestReentrantReadWriteLock {
private Map<String, String> cache = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
public void put(String key, String value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
wirteLock.unlock();
}
}
public String get(String key) {
readLock.lock();
try {
return cache.getOrDefault(key, "0");
} finally {
readLock.unlock();
}
}
}
分析源码:
ReentrantReadWriteLock实现ReadWriteLock接口
public interface ReadWriteLock {
//返回读锁
Lock readLock();
//返回写锁
Lock writeLock();
}
在ReentrantLock中,提供readLock和writeLock的实现方法。构造函数提供支持公平锁和非公平锁,默认为非公平锁。
//field
//ReentrantLock内部类实现的ReadLock、WriteLock
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
//method
//返回对应的读写锁
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
//构造函数
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
读锁(锁资源共享):
public void lock() {
//sync是基于AQS实现的公平锁或非公平锁
sync.acquireShared(1);
}
//AQS提供的获取读锁框架,tryAcquireShared为具体子类实现的获取方法
//根据state状态来识别:锁是否被持有?是否被读锁持有?然后尝试获取锁
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//为什么写锁被非当前线程持有时,才会返回呢?读-写不是互斥吗?
//假如:某个线程获取到写锁后,业务处理仍然需要读取某些共享数据时,同线程写-读互斥不就完了。
//这行代码体现了ReentrantReadWriteLock支持锁降级特性
//return -1,则该节点将在AQS获取共享锁框架中入队,如果是head后继则会再次尝试
//调用acquiredShared获取锁;非head后继则尝试park
//注意:1、ReentrantReadWriteLock不支持锁升级(拥有读锁申请写锁)
// 2、在锁升级后,释放写锁,其他线程读锁线程就可以获取读锁
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
//readerShouldBlock()在公平锁和非公平锁中提供的策略是不一样的。
//非公平锁中,head后继节点是EXCLUSIVE类型的,说明存在写锁获取锁,在读写锁中且非公平锁条件下,不阻塞读锁,会导致写锁饥饿,除过这种场景外,其他场景都不会阻塞读锁获取
//公平锁中,如果双向链表为空、(非空&&head后继==null)、(非空&&head后继是当前线程)时,return false,可以尝试获取锁资源
//SHARED_UNIT为65536,读写锁是将int state属性值得高16位作为读锁,低16位作为写锁
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//r==0表示读锁为0,没有线程获取读锁
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//r!=0且firstReader==current说明firstReaderHoldCount是自己重入次数
} else if (firstReader == current) {
firstReaderHoldCount++;
//r!=0且firstReader!=current则需要获取自身的重入次数
} else {
HoldCounter rh = cachedHoldCounter;
//rh!=null&&rh.tid跟cachedHoldeCounter不一样说明上个读锁线程不是自己,则通过ThreadLocal获取自己的重入次数
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
//rh!=null&&rh.tid==current,说明就是自己了
else if (rh.count == 0)
readHolds.set(rh);
//重入次数++
rh.count++;
}
return 1;
}
//当读锁判断后需要阻塞时
return fullTryAcquireShared(current);
}
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
//持有写锁的线程不是当前线程,返回-1.后续加入双向链表中
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
//没有互斥写锁,但是需要根据公平或非公平锁判断是否需要阻塞
//1、是公平锁时,前置节点非当前线程,但是正在执行的线程是当前线程,可以尝试获取
//2、非公平锁时,当head后继节点是互斥锁时,需要阻塞,但是正在执行的线程是当前线程,则可以尝试获取锁。如:写操作时,需要读锁获取某个值,拒绝读锁获取数据岂不是就死锁了
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
//当读锁不是第一次获取读时,需要尝试获取读锁。可重入。否则就会容易引起死锁。而如果是第一次尝试获取读锁的话,直接退出return -1,进入AQS等待链表
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");
//尝试获取读锁,并return 1
if (compareAndSetState(c, c + SHARED_UNIT)) {
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;
}
}
}
从上述代码可以看出,不管是否是公平锁,有一个问题是例外。如果当前正在执行的已拥有读锁或写锁的线程再次获取读锁时,都需要循环获取读锁,否则读锁一直获取不到读锁加入AQS链表中,那么该线程将会阻塞,严重影响性能。
private void doAcquireShared(int arg) {
//将node节点加入AQS链表
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
//线程没有被阻塞或没有获取到锁,一直循环
for (;;) {
//加入到链表后,前驱接点是head,说明它是第一个元素,则尝试获取锁
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
//获取成功,因为是读锁所有需要直接唤醒下一个节点,让下一个节点也开始获取锁
//否则就不是读-读互斥了
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
读锁到这块差不多结束了,看得头昏脑涨的……
写锁结构跟ReentrantLock完全一样啊,当然tryAcquire需要子类自己实现了。首先尝试获取锁,获取失败进入链表,再判断是否为head后继,如果是则再次tryAcquire尝试获取锁,如果不是则看前继节点是否能安全的park,并检测中断属性值。
//写锁结构跟ReentrantLock完全一样,只需要关注tryAcquire(arg)即可
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//锁的状态
int c = getState();
//互斥锁的状态
int w = exclusiveCount(c);
//state!=0说明,写锁前有读锁或写锁申请了锁资源
if (c != 0) {
//有锁,w==0说明,上次申请锁的是读锁;current!=线程持有锁不是当前线程
//线程持有锁只会是写锁设置的
if (w == 0 || current != getExclusiveOwnerThread())
//获取失败时,又到了进入双向链表,是否为head的后继尝试获取锁的流程
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//w!=0&¤t==getExclusveOwnerThread就是写锁重入了
setState(c + acquires);
return true;
}
//此处state==0,没有任何线程持有锁
//writerShouldBlock是公平锁和非公平锁都实现的方法
//非公平锁直接返回false,公平锁则判断空链表或head后继是当前线程返回false;
//cas设置state值
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//设置当前线程独占锁
setExclusiveOwnerThread(current);
return true;
}
Condition:
只有写锁支持Condition,读锁会抛出UnsupportedOperationException异常,读锁不支持Condition的原因,个人理解跟读写锁的应用场景有关,支持高并发的读。读之间都要支持Condition的话,读写锁实现会非常复杂,而且又丢了读写锁的应用场景。
读锁和写锁释放锁的逻辑就不细读了,大致就是:一直尝试释放锁,然后唤醒head后继节点,接着让它尝试获取锁的过程。
未完待续,去玩会再说……