ReentrantReadWriteLock编码示例
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();//①
//②
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
//读锁
r.lock();//③
try {
//do something
} finally {
r.unlock(); //④
}
//写锁
w.lock();//⑤
try {
//do something
} finally {
w.unlock(); //⑥
}
上面的代码展示读写锁的使用,读写锁的介绍参考Java锁,一些与ReentrantLock相同的概念可以参考ReentrantLock源码解析。
①new ReentrantReadWriteLock()
无参数构造函数初始化非公平锁。
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
//无参数构造函数初始化非公平锁
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
②rwl.readLock()和rwl.writeLock()
读锁和写锁是读写锁的不同视图。
//读锁和写锁是读写锁中sync的不同视图
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
Sync类和Sync的内部子类ThreadLocalHoldCounter
- 与ReentrantLock类似,ReentrantReadWriteLock中的内部类Sync同样基于AQS框架实现,不过这里因为要区分读锁和写锁两种模式的锁,状态的整型变量state,被拆分为两部分,高16位代表着读锁数量,低16位是写锁。
- ThreadLocalHoldCounter类继承实现ThreadLocal,用于保存每条线程自己所持有的读锁的数量。每条线程保存自己持有读锁的数量,在释放的时候只释放自己持有数量的锁,而不能释放其他线程的读锁。
读锁
③r.lock()
第一次调用tryAcquireShared()尝试获取锁。下面代码中的readerShouldBlock()函数,公平锁和非公平锁实现机制不同,也是两者区别所在。如果公平锁需要排队,非公平锁可以直接尝试加锁。
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
//获取读锁数量
int r = sharedCount(c);
//对state高16位加1,并且判断是否可以增加读锁
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//如果读锁数量为0,无读锁,当前线程是第一个读锁持有者
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//当前线程持有读锁,可重入
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {//有其他线程占有读锁,设置自己线程的readHolds保存自己的读锁数量
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);
}
如果第一次尝试失败,可能是有写锁占用,或者读锁大于最大限定值,或者CAS操作失败。执行doAcquireShared()函数进行可能的第二次尝试,如果尝试成功,要继续唤醒队列中它的后继线程节点,否则进行阻塞等待其他线程唤醒自己。
private void doAcquireShared(int arg) {
//第一次尝试失败后,入队等待
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
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);
}
}
④r.unlock()
释放锁与读锁相反,怎么加锁的就怎么释放锁。
唯一需要注意的是释放锁之后同样需要唤醒在等待队列中的后继线程节点。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);//唤醒后继节点
}
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;// loop on failed CAS
}
if (h == head)// loop if head changed
break;
}
}
写锁
写锁具有排他性,与ReentrantLock锁相似,复用相同的AQS框架函数,可以参考ReentrantLock源码解析。
⑤w.lock()
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
⑥w.unlock()
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
总结
总结一下读写锁ReentrantReadWriteLock实现逻辑:
- 内部类Sync维护的state被拆分为两部分,高16位代表着读锁数量,低16位是写锁。
- 定义ThreadLocalHoldCounter类保存每条线程自己所持有的读锁的数量。
- 读锁可以多个线程同时读,而写锁只能有一个线程。
- 读锁获得锁之后需要唤醒后继节点。这里涉及一个责任链模式,每个被唤醒并且获取读锁的线程都会继续传递尝试唤醒它的后继节点。
- 读写锁ReentrantReadWriteLock存在一个问题,如果读线程很多,并且已经占有着读锁,那么写线程请求的写锁就会迟迟得不到满足,产生线程饥饿。在Java8中引入新的StampedLock(可参考JDK8的一种新的读写锁StampedLock)对读写锁进行了升级。
网友评论