java 共享锁_JAVA并发(9)— 共享锁的获取与释放

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节点是第一个排队节点。

公平锁:第一个排队节点(无论是共享节点还是独占节点)的线程若是当前线程,那么它不需要排队;

非公平锁:排队节点的线程若是共享锁节点,就不需要排队。

802dcb94f590?utm_campaign=hugo

共享锁的公平锁和非公平锁.png

锁降级流程

上述代码中(2)表示锁支持锁降级。也就是说若当前线程持有独占锁,且当前线程再次请求共享锁时。

802dcb94f590?utm_campaign=hugo

锁降级流程.png

但若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区分节点是共享节点还是独占节点的关键;

获取锁后,传播唤醒共享节点

独占锁唤醒后,会争夺撕锁并修改头节点:

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,那什么时候会出现这种情况呢?

802dcb94f590?utm_campaign=hugo

AQS队列.png

唤醒的线程没有修改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;

}

}

802dcb94f590?utm_campaign=hugo

唤醒风暴流程图.png

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;

}

}

文章参考

相关阅读

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值