ReentrantReadWriteLock源码分析

文章目录


概述

ReentrantReadWriteLock(读写锁)是对于ReentranLock(可重入锁)的一种改进,在可重入锁的基础上,进行了读写分离。适用于读多写少的场景,对于读取写入操作分别加锁。其中读取与读取操作同步,读取和写入,写入和写入操作互斥。并且支持写锁降级的机制。

ReentrantReadWriteLock的体系结构:

  • 实现了ReadWriteLock接口,定义了读锁和写锁的模版方法,在ReentrantReadWriteLock分别进行实现(ReentrantReadWriteLock内部实现了两把锁)。
  • sync属性,实现了AQS的规范。
  • ReadLock和WriteLock都实现了Lock接口,Lock接口作为可重入锁的模版,定义了共有的行为。


实现了读写锁的规范接口

sync属性,是一个静态内部类,继承了AQS,AQS是一种规范,抽象的队列式同步器
MESA管程模型,入口等待队列用于互斥,条件变量等待队列用于同步
ReentrantReadWriteLock中的写锁,实现了Lock接口
ReentrantReadWriteLock中的读锁,同样也实现了Lock接口
Lock接口,规范了锁的实现

ReentrantReadWriteLock 同样支持公平锁和非公平锁的实现

一、状态位设计

ReentrantReadWriteLock的内部类Sync,重写了父类AQS的acquireShared方法,定义了读锁的共享、可重入特性,但是AQS的state状态位,只能表示同步状态,不能同时维护读锁、写锁的状态。在读写锁的实现中,对于state状态位,实现了按位切割的算法:

  • 低 16 位(低 2 字节):存储 写锁的重入次数
  • 高 16 位(高 2 字节):存储 读锁的获取次数

为什么写锁存储的是重入次数?写锁通常是互斥的,但有时一个线程可能会多次请求写锁(即重入)所以需要统计的是,某个线程重入了几次写锁。读锁存储的是获取次数?读锁是同步的,一般不会某个线程多次请求读锁,所以需要统计的是当前有多少个线程持有读锁

Sync类中,定义了状态位的相关属性,以及获取读,写计数的方法:

java 复制代码
		// 读锁偏移 16 位,读锁存储在 state 的高 16 位。
     static final int SHARED_SHIFT   = 16;
     // 读锁的单位值(1 左移 16 位,即 65536)
     static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
     //最大值 (1 左移 16 位 - 1,即 65535)
     static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
     //用于提取 低 16 位。
     static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
     /** Returns the number of shared holds represented in count  */
     //从 state 变量中提取高 16 位,即 读锁的获取次数。
     static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
     /** Returns the number of exclusive holds represented in count  */
     // 按位与 & 操作 提取 低 16 位,即 写锁的重入次数。
     static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

用一个案例进行说明:

  1. 假设初始 state = 0(没有读锁和写锁)

state = 0x00000000 (0000 0000 0000 0000 0000 0000 0000 0000)

  • 写锁(低 16 位):exclusiveCount(state) = state & 0000 0000 0000 0000 1111 1111 1111 1111 = 0
  • 读锁(高 16 位):sharedCount(state) = state >>> 16 = 0
  1. 假设写锁重入 3 次,此时 state = 3

state = 0x00000003 (0000 0000 0000 0000 0000 0000 0000 0011)

  • 写锁(低 16 位):

exclusiveCount(state) = state & 0000 0000 0000 0000 1111 1111 1111 1111

= 00000000 00000000 00000000 00000011 & 0000 0000 0000 0000 1111 1111 1111 1111

= 00000000 00000000 00000000 00000011 (3)

  • 读锁(高 16 位):

sharedCount(state) = state >>> 16

= 00000000 00000000 00000000 00000011 >>> 16

= 00000000 00000000 00000000 00000000 (0)

二、读锁

读锁的特点是共享,也就是读锁和读锁之间不互斥,并且可重入。那么在源码的层面,是如何定义共享、可重入的?
采用sync的acquireShared

案例代码:

java 复制代码
public class Demo1 {
    
    private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();


    public static void main(String[] args) {
        readLock.lock();
        System.out.println("第一次获取到读锁");
        try {
            readLock.lock();
            System.out.println("第二次获取到读锁");
            try {
                System.out.println("重复获取读锁");
            }finally {
                readLock.unlock();
                System.out.println("第二次获取到读锁被释放");
            }
        }finally {
            readLock.unlock();
            System.out.println("第一次获取到读锁被释放");
        }

    }
}

读锁的加锁操作:

java 复制代码
     protected final int tryAcquireShared(int unused) {
         /*
          * Walkthrough:
          * 1. If write lock held by another thread, fail.
          * 2. Otherwise, this thread is eligible for
          *    lock wrt state, so ask if it should block
          *    because of queue policy. If not, try
          *    to grant by CASing state and updating count.
          *    Note that step does not check for reentrant
          *    acquires, which is postponed to full version
          *    to avoid having to check hold count in
          *    the more typical non-reentrant case.
          * 3. If step 2 fails either because thread
          *    apparently not eligible or CAS fails or count
          *    saturated, chain to version with full retry loop.
          */
         //获取当前线程
         Thread current = Thread.currentThread();
         //获取当前线程的状态标记
         int c = getState();
         //已经有了写锁,并且写锁的拥有者不是当前线程
         if (exclusiveCount(c) != 0 &&
             getExclusiveOwnerThread() != current)
             //当前有写锁且不是当前线程持有的写锁,返回 -1,表示获取读锁失败。
             return -1;
         //得到读锁的获取次数
         int r = sharedCount(c);
         //检查当前线程是否需要等待 
         //确保当前读锁的获取次数 r 小于最大限制 MAX_COUNT
         //使用 CAS 操作来更新 state。
         if (!readerShouldBlock() &&
             r < MAX_COUNT &&
             compareAndSetState(c, c + SHARED_UNIT)) {
             //读锁的获取次数为0
             if (r == 0) {
             		 //标记当前线程为第一个获取到的
                 firstReader = current;
                 //记录第一个获取到读锁的线程的获取次数
                 firstReaderHoldCount = 1;
             //读锁的获取次数不为零,并且当前线程和第一个获取到读锁的线程相同    
             } else if (firstReader == current) {
                 //第一个获取到读锁的线程的获取次数增加
                 firstReaderHoldCount++;
             //读锁的获取次数不为零,并且当前线程不是第一个获取到读锁的线程       
             } else {
             		//从缓存中获取持有计数器
                 HoldCounter rh = cachedHoldCounter;
                 //如果缓存为空或者当前线程的 tid 不匹配
                 if (rh == null || rh.tid != getThreadId(current))
                 		//则从 readHolds 中获取新的计数器。
                     cachedHoldCounter = rh = readHolds.get();
                 //如果 rh.count == 0,说明当前线程刚开始持有读锁。
                 else if (rh.count == 0)
                 	 //更新计数器。
                     readHolds.set(rh);
                 //增加当前线程的读锁计数。
                 rh.count++;
             }
             return 1;
         }
         //如果前面的 CAS 操作失败,进行 排队等待,直到条件满足为止。
         return fullTryAcquireShared(current);
     }

这一段尝试获取读锁的代码,精髓在于使用 CAS 操作来更新 state,并且记录读锁的获取次数,是将第一个线程和后续线程分开计数的。

三、锁降级机制

如果一个线程,先获取到了写锁,然后再去获取读锁,最后释放写锁,写锁能够降级成为读锁,即:一个线程在持有写锁的情况下,将写锁转换为读锁,即允许线程在修改资源之后,在不释放锁的情况下继续读取资源(上述的操作,只针对当前线程)

java 复制代码
public class Demo2 {
    private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();


    public static void main(String[] args) {
    		 //先获取到了写锁
        writeLock.lock();
        System.out.println(Thread.currentThread().getName() + "获取到写锁");

        try {
        	  //再去获取读锁
            readLock.lock();
            System.out.println(Thread.currentThread().getName() + "获取到写锁后,又获取到了读锁");
        }finally {
            //最后释放写锁
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放了写锁");
        }

			try{
   			System.out.println("业务代码执行");
			}finally{
			   readLock.unlock();
			   System.out.println(Thread.currentThread().getName() + "释放了写锁后,释放了读锁");
			}
    }
}

  锁降级机制,特别适用于:

  • 数据修改后再查询:当某个线程对共享资源进行了修改(如数据库更新),并且希望读取最新的数据时,直接降级为读锁可以避免不必要的锁切换。
  • 避免频繁锁获取:在某些高并发场景中,多个线程需要频繁读取共享数据,但在一开始线程会进行写操作,降级为读锁可以避免重新获取写锁并减少锁的开销。

为什么要在获取写锁和释放写锁之间,去获取读锁呢?主要是为了保证数据的一致性。避免先释放写锁-再获取读锁的过程中,其他线程抢先一步获取到了写锁修改了数据。获取读锁-释放写锁,那么其他线程想要获取写锁修改数据,因为读-写锁之间的互斥,所以其他线程将被阻塞。

读锁尝试获取锁的代码中,下面的片段就是锁降级机制的体现,即已经有了写锁,并且当前线程是该写锁的持有者,那么可以继续获取读锁,不会返回-1失败:

java 复制代码
  			//已经有了写锁,并且写锁的拥有者不是当前线程
         if (exclusiveCount(c) != 0 &&
             getExclusiveOwnerThread() != current)
             //当前有写锁且不是当前线程持有的写锁,返回 -1,表示获取读锁失败。
             return -1;

四、写锁

在源码的层面,通过AQS的acquire方法,保证不同线程间写锁-写锁,读锁-写锁之间的互斥性。
  案例代码:

java 复制代码
public class Demo3 {
    private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

    public static void main(String[] args) {

        writeLock.lock();
        System.out.println(Thread.currentThread().getName() + "获取到了写锁");

        try {
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() + "再次获取到了写锁");
            try {
                System.out.println("....");
            }finally {
                writeLock.unlock();
                System.out.println(Thread.currentThread().getName() + "再次释放了写锁");
            }
        }finally {
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放了写锁");
        }
    }
}

同一线程间写锁间(写锁和读锁间)不互斥

java 复制代码
public class Demo3 {
    private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

    public static void main(String[] args) throws InterruptedException {

        writeLock.lock();
        System.out.println(Thread.currentThread().getName() + "获取到了写锁");
        Thread.sleep(3000);
        try {
            new Thread(() -> {
                writeLock.lock();
                System.out.println(Thread.currentThread().getName() + "再次获取到了写锁");
                try {
                    System.out.println("....");
                }finally {
                    writeLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "再次释放了写锁");
                }
            }).start();
        }finally {
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放了写锁");
        }
    }
}

不同线程间的写锁间互斥(写锁和读锁间也互斥)

java 复制代码
    protected final boolean tryAcquire(int acquires) {
        /*
         * Walkthrough:
         * 1. If read count nonzero or write count nonzero
         *    and owner is a different thread, fail.
         * 2. If count would saturate, fail. (This can only
         *    happen if count is already nonzero.)
         * 3. Otherwise, this thread is eligible for lock if
         *    it is either a reentrant acquire or
         *    queue policy allows it. If so, update state
         *    and set owner.
         */
        //获取当前线程
        Thread current = Thread.currentThread();
        //获取状态
        int c = getState();
        //提取当前状态 c 中的写锁计数
        int w = exclusiveCount(c);
        //状态不为0  意味着锁当前处于非空状态
        if (c != 0) {
            // (Note: if c != 0 and w == 0 then shared count != 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;
        }
        //状态为0
        //应该被阻塞 或 CAS 状态 + 重入次数 累加失败
        if (writerShouldBlock() ||
            !compareAndSetState(c, c + acquires))
            //返回加锁失败
            return false;
        //设置当前线程为锁的持有者
        setExclusiveOwnerThread(current);
        return true;
    }

总结

线程可以获取到读锁的条件:

  • 没有其他线程获取到写锁。
  • 有写锁,但是是当前线程持有(锁降级机制)

线程可以获取到写锁的条件:

  • 没有其他线程获取到读锁。
  • 没有其他线程获取到写锁。

对于同一个线程而言:读线程获取读锁后,能够再次获取读锁(不能再获取到写锁,即不支持锁升级)。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。


相关推荐
晓纪同学12 分钟前
随性研究c++-智能指针
开发语言·c++·算法
天堂的恶魔94624 分钟前
C —— 字符串操作
c语言·开发语言
徐小黑ACG26 分钟前
GO简单开发grpc
开发语言·后端·golang·grpc·protobuf
microhex27 分钟前
Javascript代码压缩混淆工具terser详解
开发语言·javascript·ecmascript
工业互联网专业37 分钟前
基于springboot+vue的二手车交易系统
java·vue.js·spring boot·毕业设计·源码·课程设计·二手车交易系统
IT技术图谱38 分钟前
【绝非标题党】Android 如何优化网络请求
java·面试
DreamByte1 小时前
Python菜鸟教程(小程序)
开发语言·python·小程序
每次的天空1 小时前
Android学习总结之Kotlin 协程
android·开发语言·kotlin
殷世杰1 小时前
springai完成mcp+知识库实现智能助手
java
froxy1 小时前
C++容器数据类型定义、测试用例
开发语言·c++·测试用例