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;
    }

总结

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

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

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

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

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


相关推荐
go54631584653 分钟前
如何使用深度学习中的 Transformer 算法进行视频目标检测
开发语言·python
java1234_小锋6 分钟前
Mybatis是如何进行分页的?
java·开发语言
wlyang6661 小时前
5. scala高阶之traits
大数据·开发语言·scala
来恩10031 小时前
C# 数组、索引器与集合介绍
开发语言·c#
游王子1 小时前
Python NumPy(12):NumPy 字节交换、NumPy 副本和视图、NumPy 矩阵库(Matrix)
开发语言·python·numpy
来恩10031 小时前
C# 委托与事件介绍
开发语言·c#
苹果酱05671 小时前
Java 微服务实用指南(一)
java·spring boot·毕业设计·layui·课程设计
老马啸西风1 小时前
IM 即时通讯系统-46-OpenIM 提供了专为开发者设计的开源即时通讯解决方案
java·分布式·开源·im
我是苏苏1 小时前
python开发:爬虫示例——GET和POST请求处理
开发语言·爬虫·python
leluckys2 小时前
swift 专题三 swift 规范一
开发语言·ios·swift