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);  
}  
  1. 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节点是第一个排队节点。

  • 公平锁:第一个排队节点(无论是共享节点还是独占节点)的线程若是当前线程,那么它不需要排队;
  • 非公平锁:排队节点的线程若是共享锁节点,就不需要排队。
JAVA并发(9)— 共享锁的获取与释放_第1张图片
共享锁的公平锁和非公平锁.png
  1. 锁降级流程

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

JAVA并发(9)— 共享锁的获取与释放_第2张图片
锁降级流程.png

但若AQS队列中存在2个即以上的节点,且head.next节点不为共享节点,那么readerShouldBlock需要排队。即会直接执行fullTryAcquireShared方法,而不会进行锁降级。

  1. 共享锁记录线程信息与共享锁的重入数

在独占锁流程中,有两个类属性:

  1. status:记录的是锁是否被占有,以及该线程的重入数;
  2. exclusiveOwnerThread:记录持有独占锁的线程;

共享锁流程,有几个类属性:

  1. status:记录锁是否被占有,共享锁是使用高16位标识来计算。
  2. firstReaderHoldCount:(int类型),记录获取共享锁第一个线程的可重入数;
  3. firstReader:(Thread类型),记录获取共享锁第一个线程的线程信息;
  4. cachedHoldCounter:(HoldCounter类型),本质上是int类型+Thread类型,记录获
    取共享锁最后一个线程的线程信息以及重入数。
  5. readHolds:(ThreadLocalHoldCounter类型),将HoldCounter对象通过ThreadLocal保存在每个线程的ThreadLocalMap中。
  1. 多个线程抢夺共享锁失败后如何处理?

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(如果需要排队){

}
开始争抢锁。

若当前线程已经持有了独占锁,且当前线程还要获取共享锁时,那么会让该线程再次获取共享锁。执行锁降级

  1. 若当前线程需要排队

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. 然后判断自己是否需要排队,当然对于共享锁来说也有公平锁非公平锁之分。
  3. 若自己不需要排队,那么就开始争抢锁;
  4. 若争抢锁失败,或自己需要排队,即调用操作系统将自己挂起。

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);  
    }  
}  
  1. 将线程加入到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方法详解...

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

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

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

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;  
    }  
}  
JAVA并发(9)— 共享锁的获取与释放_第3张图片
唤醒风暴流程图.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;  
     }  
 }  

文章参考

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实现生产者消费者

你可能感兴趣的:(JAVA并发(9)— 共享锁的获取与释放)