public static void main(String[] args) {
ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
//共享锁获取
lock.readLock().lock();
//共享锁的释放
lock.readLock().unlock();
}
在ReentrantLock中,不仅存在独占锁,而且还存在共享锁(即多个线程可以获取到锁)
1. 共享锁的获取
//共享锁的获取
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
作为对比,我们可以看一下独占锁的获取
//独占锁的获取
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1.1 尝试获取锁tryAcquireShared(arg)
首先明确的是:无论是独占锁还是共享锁,他们都是依靠一个状态位(status)来标识上锁状态。
只不过共享锁使用高16位
来记录锁的状态。
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
//1. 获取当前的状态
int c = getState();
//2. 判断是否独占锁被占用(锁降级)
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//3. 获取读锁计数
int r = sharedCount(c);
//4. 尝试获取锁,多个读锁只会有一个成功,不成功会进入fullTryAcquireShared重试
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//因为使用CAS获取锁,所以此处线程安全。
//5. 第一个线程获取到读锁
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//6. 如果当前线程是第一个获取读锁的线程,那么直接后 firstReader+1
} else if (firstReader == current) {
firstReaderHoldCount++;
//7. 记录最后一个获取读锁的线程或记录其他线程读锁的重入数。
} else {
HoldCounter rh = cachedHoldCounter;
//HoldCounter为null或HoldCounter记录的当前线程不是之前存储的。
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);
}
- readerShouldBlock()方法如何决定是否排队?
而判定是否排队,公平锁和非公平锁有自己的处理逻辑:
//非公平锁处理逻辑
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
//公平锁的处理逻辑
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
总结下:只有AQS队列中最少存在2个及以上
的节点时。head节点为正在处理业务的节点,head.next节点是第一个排队节点。
- 公平锁:第一个排队节点(无论是共享节点还是独占节点)的线程若是当前线程,那么它不需要排队;
- 非公平锁:排队节点的线程若是共享锁节点,就不需要排队。
- 锁降级流程
上述代码中(2)表示锁支持锁降级
。也就是说若当前线程持有独占锁
,且当前线程再次请求共享锁
时。
但若AQS队列中存在2个即以上的节点,且head.next节点不为共享节点,那么readerShouldBlock
需要排队。即会直接执行fullTryAcquireShared
方法,而不会进行锁降级。
- 共享锁记录线程信息与共享锁的重入数
在独占锁流程中,有两个类属性:
- status:记录的是锁是否被占有,以及该线程的重入数;
- exclusiveOwnerThread:记录持有独占锁的线程;
共享锁流程,有几个类属性:
- status:记录锁是否被占有,共享锁是使用高16位标识来计算。
- firstReaderHoldCount:
(int类型)
,记录获取共享锁第一个线程的可重入数; - firstReader:
(Thread类型)
,记录获取共享锁第一个线程的线程信息; - cachedHoldCounter:
(HoldCounter类型)
,本质上是int类型+Thread类型
,记录获
取共享锁最后一个线程的线程信息以及重入数。 - readHolds:
(ThreadLocalHoldCounter类型)
,将HoldCounter
对象通过ThreadLocal
保存在每个线程的ThreadLocalMap
中。
- 多个线程抢夺共享锁失败后如何处理?
compareAndSetState(c, c + SHARED_UNIT)
方法的含义是同一时刻只能有一个线程执行成功获取共享锁,那么执行失败的线程只能通过自旋+CAS
再次获取共享锁了。
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
//开启自旋
for (;;) {
int c = getState();
//该锁被独占锁持有,那么直接加锁失败。
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
//如果读锁应当被阻塞
} else if (readerShouldBlock()) {
// 判断当前线程是否是第一个读锁线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
// cachedHoldCounter记录的是最后一个读锁线程。
rh = cachedHoldCounter;
//如果最后一个不是当前线程
if (rh == null || rh.tid != getThreadId(current)) {
//在线程中取出HoldCounter对象。
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
//若是HoldCounter为0,加锁失败
if (rh.count == 0)
return -1;
}
}
//读锁超出最大数量
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//开始抢夺读锁
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;
}
}
}
在该代码中仍然有锁降级的代码:
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if(如果需要排队){
}
开始争抢锁。
若当前线程已经持有了独占锁,且当前线程还要获取共享锁时,那么会让该线程再次获取共享锁。执行锁降级
- 若当前线程需要排队
若tryAcquireShared
最终结果返回1,那么证明该线程获取到共享锁。若是返回-1,那么会执行doAcquireShared(arg);
方法,加入到AQS队列后进行阻塞。
HoldCounter rh = null;
else if (readerShouldBlock()) {
// 如果当前线程是第一个获取共享锁的线程。
if (firstReader == current) {
// 直接取获取共享锁
} else {
if (rh == null) {
//最后一个获取共享锁的线程计数器
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
//获取线程中ThreadLocalMap携带的线程计数器
rh = readHolds.get();
//若此时线程计数器中数量为0,线程移除ThreadLocal
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
流程总结:
- 线程进入后,它会先判断
当前锁
是否被独占
。若不是自己独占的,那么就会去排队。 - 然后判断自己是否需要
排队
,当然对于共享锁
来说也有公平锁
和非公平锁
之分。 - 若自己不需要排队,那么就开始争抢锁;
- 若争抢锁失败,或自己需要排队,即调用操作系统将自己挂起。
1.2 排队并阻塞doAcquireShared
private void doAcquireShared(int arg) {
// 1. 将自己加入到AQS中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//2. 判断node的上一个节点
final Node p = node.predecessor();
if (p == head) {
//3. 尝试获取锁
int r = tryAcquireShared(arg);
if (r >= 0) {
// 4. 修改head节点,并传播唤醒后续共享节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//5. 获取锁失败后,修改node节点的ws属性 && 阻塞当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 将线程加入到AQS中。
//共享锁将当前线程加入到AQS中
static final Node SHARED = new Node();
final Node node = addWaiter(Node.SHARED);
//独占锁将当前线程加入到AQS中
static final Node EXCLUSIVE = null;
addWaiter(Node.EXCLUSIVE)
他们的逻辑相同,但是参数不同,独占锁的参数为null,而共享锁的参数为new Node();
而这,就是AQS区分节点是共享节点还是独占节点的关键;
addWaiter方法详解...
- 获取锁后,传播唤醒共享节点
独占锁唤醒后,会争夺撕锁并修改头节点:
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
共享锁唤醒后,争夺锁修改头节点并唤醒之后的共享节点:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//修改头节点
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果head节点后的节点为共享节点
if (s == null || s.isShared())
doReleaseShared();
}
}
独占锁,独占锁被释放后,才会唤醒后续节点的线程。而共享锁,节点获取到锁以及节点释放锁时都会唤醒后续节点的线程。
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
//唤醒h.next节点
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
该方法中断条件
该方法可以看做为唤醒风暴
。它是一个自旋
方法,方法的出口就是h==head
,那什么时候会出现这种情况呢?
唤醒的线程没有修改head
节点,即唤醒的线程没有获取到锁
。此时h==head
成立。该线程不会再唤醒head.next
节点线程。
唤醒的线程若是独占锁线程,那么它肯定不会得到锁。
什么叫做唤醒风暴?
线程A唤醒线程B,若线程B获取到了锁,并修改了head节点,那么该节点一定为共享节点。那么线程A还是会唤醒head.next
节点的。
当然线程B也会唤醒head.next
节点线程。这样的话,大量的线程会去唤醒AQS队列中的共享节点(直至遇到独占节点后终止唤醒)。这就是唤醒风暴
。
节点的waitStatus状态动态改变。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
维护AQS队列和修改node的waitStatus
属性是两个操作。tail节点若刚成为head节点,此时新node节点刚进行cas-tail操作成功新的tail节点,但新的node节点还未改变前驱节点
的waitStatus
属性。
针对于刚进入的节点,唤醒风暴
也会将其唤醒。
private void doReleaseShared() {
for (;;) {
Node h = head;
//1. tail节点成为了head节点,且此时新node节点入队。进行了cas-tail,但未执行cas-ws操作
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//唤醒h.next节点
unparkSuccessor(h);
}
// 2. 因为head节点的ws此时还是0,那么条件1成立;
// 3. 执行条件2时,新node节点执行了cas-ws状态,此时head的ws=-1,执行失败。会继续自旋。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
2. 解锁过程
public final boolean releaseShared(int arg) {
//重点看下这个方法,修改节点的status属性
if (tryReleaseShared(arg)) {
//前面已经具体分析-该方法为唤醒风暴方法。
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//修改线程计数器中线程重入的数量,因为修改的是ThreadLocal中的value,所以不存在并发问题。
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;
}
//自旋式修改status状态,也就是-1。(存在并发问题)
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
文章参考
https://www.jianshu.com/p/cd485e16456e
https://www.cnblogs.com/huangjuncong/p/9183761.html
https://segmentfault.com/a/1190000016447307
相关阅读
JAVA并发(1)—java对象布局
JAVA并发(2)—PV机制与monitor(管程)机制
JAVA并发(3)—线程运行时发生GC,会回收ThreadLocal弱引用的key吗?
JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!
JAVA并发(5)— 多线程顺序的打印出A,B,C(线程间的协作)
JAVA并发(6)— AQS源码解析(独占锁-加锁过程)
JAVA并发(7)—AQS源码解析(独占锁-解锁过程)
JAVA并发(8)—AQS公平锁为什么会比非公平锁效率低(源码分析)
JAVA并发(9)— 共享锁的获取与释放
JAVA并发(10)—interrupt唤醒挂起线程
JAVA并发(11)—AQS源码Condition阻塞和唤醒
JAVA并发(12)— Lock实现生产者消费者