阿亮的日志

  • 首页
  • 分类
  • 归档
  • 关于

  • 搜索
事务 feign b 有序性 原子性 可见性 volatile 并发编程 hystrix 源码 eureka springcloud JVM

7.读写锁ReentrantReadWriteLock原理

发表于 2021-03-17 | 分类于 并发编程 | 0 | 阅读次数 31

前言

本篇主要来学习读写锁相关的一些知识。我们在并发中经常用到的是读多写少的场景,如果这时候使用读写锁分离的方式,能够大大提高我们的性能

知识点

读写锁之间非常巧妙的使用了state的值,其中高16位代表读锁,低16位代表写锁

1.读写锁的实现

读写锁。既可以加读锁,也可以加写锁。

  • 读写锁之间彼此互斥:加了读锁不能加写锁,加了写锁不能加读锁
  • 写锁与写锁之间互斥:加了写锁之后其他写锁也会被挂起,入队
  • 读锁与读锁之间共享:加了读锁之后,其他读锁也能获取到锁

2.ReentrantReadWriteLock的实现(AQS)

2.1.写锁

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);//低16位的值。写锁
    if (c != 0) {//加过锁
        // (Note: if c != 0 and w == 0 then shared count != 0)
        //写锁为0,加的是读锁;或者写锁不为0,且当前线程不是已获取独占锁的线程,锁获取失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    //
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

从此处可以看到,通过判断state是否为0,若为0,则说明已经加过锁。

  • w即低15位:写锁的标志位。若w==0(加的是读锁)或者或者当前线程不是加锁线程,否则加入可重入锁
  • 若没有加过锁,则直接进行加锁 writerShouldBlock():
    • 非公平锁直接返回false,即不需要阻塞
    • 公平锁判定是否有前任hasQueuedPredecessors,然后CAS修改状态,若修改成功,设置当前线程为exclusiveOwnerThread,即占有锁的线程

2.2.可重入

假设线程1已经加过写锁,此时线程1再次获取写锁,那么可重入是如何实现的呢?

if (c != 0) {//加过锁
    // (Note: if c != 0 and w == 0 then shared count != 0)
    //写锁为0,加的是读锁;或者写锁不为0,且当前线程不是已获取独占锁的线程,锁获取失败
    if (w == 0 || current != getExclusiveOwnerThread())
        return false;
    if (w + exclusiveCount(acquires) > MAX_COUNT)
        throw new Error("Maximum lock count exceeded");
    // Reentrant acquire
    setState(c + acquires);
    return true;
}

此时state!=0,则c!=0,w!=0(写锁),说明低16位不为0,则只需setState(c + acquires);代码,此时state=2,由此可以看出如下结论

  • 通过r(读锁),w(写锁)判断高低16位有没有加过读锁或者写锁,并且通过判段高低16位的值,来判断读锁和写锁的重入次数。
  • 此时state的值不一定是2,如果既有写锁又有读锁的情况下,state的值可能是非常大,state的低16位的值才代表写锁的重入次数。

2.3.写锁失败时入队

当线程写锁失败时候,需要入队,代码逻辑如下


public final void acquire(int arg) {
    //尝试加锁
    if (!tryAcquire(arg) &&
        //加锁失败
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //此处将之前的节点状态修改为-1
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //挂起当前线程
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  • 线程1加了写锁,Node的waitestatus=-1,当前低16位因为线程1加了两次锁,state=2,锁占用的线程是线程1。head=tail=线程1的node
  • 线程2请求加写锁,此时加锁失败,会在tail后面拼接一个新的node,waiteStatus=0

2.4.读写锁互斥

写锁-->读锁的互斥

protected final boolean tryAcquire(int acquires) {
 .......
if (c != 0) {
    //
    if (w == 0 || current != getExclusiveOwnerThread())
        return false;
    if (w + exclusiveCount(acquires) > MAX_COUNT)
        throw new Error("Maximum lock count exceeded");
    // Reentrant acquire
    setState(c + acquires);
    return true;
}
if (writerShouldBlock() ||
    !compareAndSetState(c, c + acquires))
    return false;
setExclusiveOwnerThread(current);

return true;
}
  • c即state不为0,w==0(写锁为0,即加的是读锁),返回false,加锁失败。入队

读锁-->写锁的互斥

protected final int tryAcquireShared(int unused) {
   
    int c = getState();
    //低16位加锁了,及已经加了写锁,且不是当前线程,失败入队
    if (exclusiveCount(c) != 0 &&
    getExclusiveOwnerThread() != current)
    return -1;
return 1;
          

2.5.锁的释放

写锁的释放

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //唤醒继任者
            unparkSuccessor(h);
        return true;
    }
    return false;
}
//解锁的逻辑
protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //当前state的值减去1,解锁1次,减一(可重入)
    int nextc = getState() - releases;
    //低16位为0,则说明解锁成功
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}
  • 尝试释放锁成功的话,如果头结点不为空,并且头结点waiteStatus!=0(0代表未解锁成功),则唤醒继任者
  • 释放锁:将state进行减一操作,若操作后state的低16位为0,则解锁成功。

读锁的释放

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;
    }
  • 处理高16位的值之后判断是否为0,若为0则解锁成功

  • Github: https://github.com/liangliang1259/common-notes

  • 公众号

坚持有质量的创作,您的支持将支持我继续创作!
阿亮 微信支付

微信支付

阿亮 支付宝

支付宝

  • 本文作者: 阿亮
  • 本文链接: http://sunliangliang.com/?p=45
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# 事务 # feign # b # 有序性 # 原子性 # 可见性 # volatile # 并发编程 # hystrix # 源码 # eureka # springcloud # JVM
6.ReentrantLock与AQS的原理
8.ThreadLocal的应用及源码
  • 文章目录
  • 站点概览
阿亮

阿亮

26 日志
11 分类
13 标签
RSS
Github E-mail
Creative Commons
0%
© 2021 阿亮
由 Halo 强力驱动
|
主题 - NexT.Pisces v5.1.4