图解ReentrantReadWriteLock读写锁的实现原理(上)

简介: 图解ReentrantReadWriteLock读写锁的实现原理

概述


ReentrantReadWriteLock读写锁是使用AQS的集大成者,用了独占模式和共享模式。本文和大家一起理解下ReentrantReadWriteLock读写锁的实现原理。在这之前建议大家阅读下下面3篇关联文章:

深入浅出理解Java并发AQS的独占锁模式

深入浅出理解Java并发AQS的共享锁模式

通俗易懂读写锁ReentrantReadWriteLock的使用


原理概述


1671194837883.jpg

上图是ReentrantReadWriteLock读写锁的类结构图:

  • 实现了ReadWriteLock接口,该接口提供了获取读锁和写锁的API。
  • ReentrantReadWriteLock读写锁内部的成员变量readLock是读锁,指向内部类ReadLock。
  • ReentrantReadWriteLock读写锁内部的成员变量writeLock是写锁,指向内部类WriteLock。
  • ReentrantReadWriteLock读写锁内部的成员变量sync是继承AQS的同步器,他有两个子类FairSync公平同步器和NoFairSync非公平同步器,读写锁内部也有一个sync,他们使用的是同一个sync。

读写锁用的同一个sync同步器,那么他们共享同一个state, 这样不会混淆吗?

不会,ReentrantReadWriteLock读写锁使用了AQS中state值得低16位表示写锁得计数,用高16位表示读锁得计数,这样就可以使用同一个AQS同时管理读锁和写锁。

  1. ReentrantReadWriteLock类重要成员变量
// 读锁
private final ReentrantReadWriteLock.ReadLock readerLock;
// 写锁
private final ReentrantReadWriteLock.WriteLock writerLock;
// 同步器
final Sync sync;
  1. ReentrantReadWriteLock构造方法
//默认是非公平锁,可以指定参数创建公平锁
public ReentrantReadWriteLock(boolean fair) {
    // true 为公平锁
    sync = fair ? new FairSync() : new NonfairSync();
    // 这两个 lock 共享同一个 sync 实例,都是由 ReentrantReadWriteLock 的 sync 提供同步实现
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}
  1. Sync类重要成员变量
// 用来移位
static final int SHARED_SHIFT   = 16;
// 高16位的1
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
// 65535,16个1,代表写锁的最大重入次数
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
// 低16位掩码:0b 1111 1111 1111 1111,用来获取写锁重入的次数
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 获取读写锁的读锁分配的总次数
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
// 写锁(独占)锁的重入次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }


加锁原理


图解过程


设计一个加锁场景,t1线程加写锁,t2线程加读锁,我们看下它们整个加锁得流程。

  1. t1 加写锁w.lock()成功,占了 state 的低 16 位。

1671194865451.jpg

  • 这里得state分为两部分0_1,0表示高16位的值,1表示低16位的值。
  • AQS当前占用线程exclusiveOwnerThread属性指向t1线程。
  1. t2线程执行加读锁 r.lock(),尝试获取锁,发现已经被写锁占据了,加锁失败。

1671194873549.jpg

  1. t2线程被封装成一个共享模式Node.SHARED的节点,加入到AQS的队列中。

1671194882240.jpg

  1. 在阻塞前,t2线程发现自己是队列中的老二,会尝试再次获取读锁,因为t1没有释放,它会失败,然后它会把队列的前驱节点的状态改为-1,然后阻塞自身,也就是t2线程。

1671194916339.jpg

  • 上面中黄色三角形就是等待状态的值,前驱节点变成-1
  • 上面中的灰色表示节点所在的线程阻塞了
  1. 后面如过有其他线程如t3,t4加读锁或者写锁,由于t1线程没有释放锁,会变成下面的状态。

1671194923571.jpg

上面是整个解锁的流程,下面深入源码验证这个流程。


源码解析


  1. 写锁加锁源码

WriteLock类的lock()方法是加写锁的入口方法。

static final class NonfairSync extends Sync {
    // ... 省略无关代码
    // 外部类 WriteLock 方法, 方便阅读, 放在此处
    public void lock() {
        sync.acquire(1);
    }
    // AQS 继承过来的方法, 方便阅读, 放在此处
    public final void acquire(int arg) {
        if (
            // 尝试获得写锁失败
                !tryAcquire(arg) &&
                        // 将当前线程关联到一个 Node 对象上, 模式为独占模式
                        // 进入 AQS 队列阻塞
                        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
        ) {
            selfInterrupt();
        }
    }
    protected final boolean tryAcquire(int acquires) {
        // 获取当前线程
        Thread current = Thread.currentThread();
        //获得锁的状态
        int c = getState();
        // 获得低 16 位, 代表写锁的 state 计数
        int w = exclusiveCount(c);
         // c不等于0表示加了读锁或者写锁
        if (c != 0) {
            if (
                // c != 0 and w == 0 表示有读锁返回错误,读锁不支持锁升级, 或者
                    w == 0 ||
                          // w != 0 说明有写锁,写锁的拥有者不是自己,获取失败
                            current != getExclusiveOwnerThread()
            ) {
                // 获得锁失败
                return false;
            }
            // 写锁计数超过低 16 位最大数量, 报异常
            if (w + exclusiveCount(acquires) > MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            // 写锁重入, 获得锁成功,没有并发,所以不使用 CAS
            setState(c + acquires);
            return true;
        }
        if (
             // c == 0,说明没有任何锁,判断写锁是否该阻塞,是 false 就尝试获取锁,失败返回 false
                writerShouldBlock() ||
                        // 尝试更改计数失败
                        !compareAndSetState(c, c + acquires)
        ) {
            // 获得锁失败
            return false;
        }
        // 获得锁成功,设置锁的持有线程为当前线程
        setExclusiveOwnerThread(current);
        return true;
    }
    // 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞
    final boolean writerShouldBlock() {
        return false; 
    }
    // 公平锁会检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
}
  • tryAcquire()方法是模板方法,由子类自定义实现获取锁的逻辑。
  • 线程如果获取写锁失败的话,通过acquireQueued()方法封装成独占Node加入到AQS队列中。

2.读锁加锁源码

ReadLock类的lock()方法是加读锁的入口方法,调用tryAcquireShared()方法尝试获取读锁,返回负数,失败,加入到队列中。

// 加读锁的方法入口
public void lock() {
    sync.acquireShared(1);
}
public final void acquireShared(int arg) {
    // tryAcquireShared 返回负数, 表示获取读锁失败,加入到队列中
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}


目录
相关文章
|
9月前
|
Java
【ReentrantReadWriteLock的实现原理】
【ReentrantReadWriteLock的实现原理】
|
11月前
|
安全
架构系列——通过ReentrantLock源码分析给对象上锁的原理
架构系列——通过ReentrantLock源码分析给对象上锁的原理
|
2月前
并发编程之读写锁ReadWriteLock的详细解析(带小案例)
并发编程之读写锁ReadWriteLock的详细解析(带小案例)
22 0
|
2月前
|
安全 Java 测试技术
ReentrantReadWriteLock(可重入读写锁)源码解读与使用
ReentrantReadWriteLock(可重入读写锁)源码解读与使用
|
2月前
|
安全 Java
ReentrantLock 原理你都知道吗?
通过以上步骤和示例代码,你应该对 ReentrantLock 的工作原理有了清晰的理解。欢迎关注威哥爱编程,一起学习成长。
|
12月前
读写锁原理解读(下)
读写锁原理解读(下)
读写锁原理解读(下)
|
8月前
图解ReentrantLock底层公平锁和非公平锁实现原理
图解ReentrantLock底层公平锁和非公平锁实现原理
88 0
图解ReentrantReadWriteLock读写锁的实现原理(下)
图解ReentrantReadWriteLock读写锁的实现原理
109 0
图解ReentrantReadWriteLock读写锁的实现原理(下)
|
安全 Java
java并发原理实战(9)--手动实现一个可重入锁
java并发原理实战(9)--手动实现一个可重入锁
105 0
java并发原理实战(9)--手动实现一个可重入锁