ReentrantReadWriteLock基础和原理

什么是ReentrantReadWriteLock?

传统的锁,比如synchronized或者ReentrantLock,无论是读还是写,同一时间只允许一个线程访问共享资源。

当读操作远多于写操作时,这种互斥会导致性能瓶颈,而多个读操作本身不会修改数据,完全可以并发执行。ReentrantReadWriteLock使得多个线程可以同时获得读锁,使得并发效率提高:

  • 读-读不互斥:多个线程可以同时获取读锁

  • 读-写互斥:有线程持有写锁时,其他线程无法获取读锁或写锁

  • 写-写互斥:同一时间只允许一个线程持有写锁

ReentrantReadWriteLock的核心特性

读写锁分离

java 复制代码
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReadLock readLock = rwLock.readLock();    // 读锁
WriteLock writeLock = rwLock.writeLock();  // 写锁

其中读锁不支持条件变量,写锁支持条件变量。

重入性

  • 读锁:允许同一线程重复获取读锁

  • 写锁:允许同一线程重复获取写锁(可重入)

  • 写锁可以降级为读锁,但读锁不能升级为写锁

当一个线程获取了读锁之后,再去获得写锁,会导致获取写锁永久等待 。 当一个线程获取了写锁之后,再去获得读锁,可以成功获取

公平性

java 复制代码
// 非公平锁(默认)
ReentrantReadWriteLock nonfairLock = new ReentrantReadWriteLock();

// 公平锁
ReentrantReadWriteLock fairLock = new ReentrantReadWriteLock(true);

内部的State含义

java 复制代码
/*  
* Read vs write count extraction constants and functions.  
* Lock state is logically divided into two unsigned shorts:  
* The lower one representing the exclusive (writer) lock hold count,  
* and the upper the shared (reader) hold count.  
*/  
  
static final int SHARED_SHIFT = 16;  
static final int SHARED_UNIT = (1 << SHARED_SHIFT);  
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;  
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;  
  
/** Returns the number of shared holds represented in count. */  
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }  
/** Returns the number of exclusive holds represented in count. */  
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

ReentrantReadWriteLock中的state用于同时维护读锁和写锁的状态:

  • 高16位:记录读锁的持有数量

  • 低16位:记录写锁的重入次数

读锁

加锁流程

tryAcquireShared

java 复制代码
public void lock() {  
    sync.acquireShared(1);  
}

读锁本质是一种共享锁 ,因此调用的是acquireShared

java 复制代码
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            acquire(null, arg, true, false, false, 0L);
    }

其内部先调用tryAcquireShared,如果未获取到锁再进入同步队列。

java 复制代码
protected final int tryAcquireShared(int unused) {
			//获取当前的线程
            Thread current = Thread.currentThread();
            int c = getState();
    		//如果有线程持有写锁并且该线程并不是本线程,则获取读锁失败,这里实现了读写互斥
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
    		//获取读锁被持有的数量
            int r = sharedCount(c);
            if (!readerShouldBlock() && //判断读线程是否应该阻塞
                r < MAX_COUNT && //判断读锁是否超限
                compareAndSetState(c, c + SHARED_UNIT)//尝试CAS
               ) {
                if (r == 0) { //如果是第一个读锁
                    firstReader = current; //记录这个线程
                    firstReaderHoldCount = 1; //并记录当前线程持有的读锁数量
                } else if (firstReader == current) {
                    firstReaderHoldCount++; //增加线程持有的读锁数量
                } else {
                    HoldCounter rh = cachedHoldCounter;//获取上一个非firstReader线程的缓存
                    //如果该缓存为空或者缓存的线程和当前线程不同
                    if (rh == null ||
                        rh.tid != LockSupport.getThreadId(current))
                        //获得或者创建当前线程的HoldCounter,并且赋给cachedHoldCounter
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)//count==0说明是新创建的
                        readHolds.set(rh);//将新创建的放入
                    rh.count++;//增加一个读锁的持有
                }
                return 1;//获取到读锁
            }
            return fullTryAcquireShared(current);
        }

总结上述源码的流程:

  1. 判断是否有线程持有写锁 ,且该线程是否是当前线程,实现了读写互斥锁降级
  2. 判断是否可以尝试获取锁
    • readerShouldBlock判断读线程是否应该阻塞。由公平/非公平策略实现的方法:
    • 锁的数量有没有越界

非公平锁 :通常检查同步队列中是否有正在等待的、比自己更早的线程(主要是写线程 )。如果队列头结点的下一个节点是请求写锁的线程,读线程可能 会阻塞,以减少"写线程饥饿"的概率。

java 复制代码
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
        final boolean apparentlyFirstQueuedIsExclusive() {
            Node h, s;
            return (h = head) != null // 头节点存在
            && (s = h.next)  != null //有第一个等待者
            &&!(s instanceof SharedNode) // 不是读线程
            && s.waiter != null; //关联的线程不为null
    }
        

为什么是可能阻塞?

因为上述的策略中,只检查第一个等待者是不是写线程,可能会出现第一个是读线程,后续的才是写线程的情况。

比如此刻一个线程获得了写锁,后续5个读线程被阻塞在同步队列中

head->(线程1)->(线程2)->(线程3)->(线程4)->(线程5)

这时又来一个写线程,因为写写互斥,因此也阻塞

head->(线程1)->(线程2)->(线程3)->(线程4)->(线程5)->(写线程6)

当当前线程释放写锁,唤醒线程1,因此线程1是共享节点,当它获得锁之后,会唤醒后续的读线程2,发生连锁反应,而如果在这个反应的过程中,新来了一个读线程7,此时写线程6还不是head的后继节点,因此这时线程7会插队,直接获得锁。

公平锁:检查同步队列中是否有任何正在等待的线程(无论读写)。如果有,则新来的读线程需要排队,保证绝对公平。

java 复制代码
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
        
  1. 最后尝试一次CAS,如果CAS失败,将调用fullTryAcquireShared
  2. 如果CAS成功
    • 判断当前线程是否是第一个读锁,如果是使用firstReader记录,并设置firstReaderHoldCount=1或者增加1
    • 如果不是,则判断是否是上一个非 firstReader线程的 HoldCounter的缓存
    • 如果都不是,通过ThreadLocal获取当前线程的HoldCounter,更新持有数量

firstReader的作用是什么?

它的主要作用是记录第一个获取读锁的线程 ,并缓存该线程的重入次数 。其核心设计目的是为了避免在某些高频场景下使用相对昂贵的 ThreadLocal查找,从而提升性能。

cachedHoldCounter是什么?

cachedHoldCounterReentrantReadWriteLock中另一个性能优化字段,它与firstReader协同工作,共同目标是减少访问ThreadLocal的次数,从而提升锁操作的效率。

以下是HoldCounter以及ThreadLocalHoldCounter的源码

java 复制代码
        static final class HoldCounter {
            int count;          // initially 0
            // Use id, not reference, to avoid garbage retention
            final long tid = LockSupport.getThreadId(Thread.currentThread());
        }

        /**
         * ThreadLocal subclass. Easiest to explicitly define for sake
         * of deserialization mechanics.
         */
        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

fullTryAcquireShared

java 复制代码
        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) {
      					//当前线程是第一个读线程,因此允许重入,不阻塞
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;//获取缓存
                            if (rh == null ||
                                rh.tid != LockSupport.getThreadId(current)) {
                                //缓存不是当前线程的,获取当前线程的缓存
                                rh = readHolds.get();
                                if (rh.count == 0) //计数为0,清理ThreadLocal
                                    readHolds.remove();
                            }
                        }
                        //计数为0,说明不持有读锁,阻塞
                        if (rh.count == 0)
                            return -1;
                    }
                }
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
				//CAS获取读锁,这部分逻辑和tryAcquireShared一样
                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 != LockSupport.getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

fullTryAcquireSharedtryAcquireShared的代码部分类似。

总结上述源码的流程:

  1. 判断是否有线程持有写锁 ,且该线程是否是当前线程,实现了读写互斥锁降级
  2. readerShouldBlock判断是否应该阻塞,需要阻塞的情况下,再判断是否是重入情况
  3. CAS获取锁,更新计数。

acquire中获取锁

如果是在同步队列中,该线程成为头节点,获取锁,还会执行

java 复制代码
             if (acquired) {
                 if (first) {
                        node.prev = null;
                        head = node;
                        pred.next = null;
                        node.waiter = null;
                        if (shared)
                            signalNextIfShared(node);
                        if (interrupted)
                            current.interrupt();
                    }
                    return 1;
                }
      private static void signalNextIfShared(Node h) {
        Node s;
        if (h != null && (s = h.next) != null &&
            (s instanceof SharedNode) && s.status != 0) {
            s.getAndUnsetStatus(WAITING);
            LockSupport.unpark(s.waiter);
        }
    }

这部分代码,如果后继节点是共享节点,那么该节点会被唤醒,形成连锁反应,直到遇到一个独占节点。

释放锁流程

java 复制代码
        public void unlock() {
            sync.releaseShared(1);
        }
        public final boolean releaseShared(int arg) {
            if (tryReleaseShared(arg)) {
                signalNext(head);
                return true;
            }
            return false;
        }

这是两个经典的方法了,重点看重写的tryReleaseShared方法。

java 复制代码
   protected final boolean tryReleaseShared(int unused) {
       		//获取当前线程,判断是否是firstReader
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                if (firstReaderHoldCount == 1)
                    //完全释放,firstReader置为null
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                //获得当前的HolderCounter
                HoldCounter rh = cachedHoldCounter;
                if (rh == null ||
                    rh.tid != LockSupport.getThreadId(current))
                    //不是cachedHoldCounter,从ThreadLocal中取出
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    //完全释放读锁,在ThreadLocal中移除
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                //持有数量减一
                --rh.count;
            }
       		//自旋cas更新state
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
     				//如果nextc==0,代表读锁和写锁全部被释放了,可以尝试唤醒写线程了
                    return nextc == 0;
            }
        }

总结上述源码的流程:

  1. 通过firstReader以及cachedHoldCount获得当前线程的HoldCount,然后更新其中的计数
  2. 通过自旋CAS来更新State,并且只有在state==0即没有任何线程拥有读写锁,才会唤醒同步队列中的线程。

写锁

加锁流程

java 复制代码
     public void lock() {
            sync.acquire(1);
     }
     public final void acquire(int arg) {
        if (!tryAcquire(arg))
            acquire(null, arg, false, false, false, 0L);
    }

重点在tryAcquire方法。

java 复制代码
protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
    		////这种情况代表当前有线程拥有锁
            if (c != 0) {
                // c!=0&&w==0代表当前有读锁,直接获取失败
                // c!=0&&w!=0代表当前有写锁,如果写锁不是当前线程的,也直接失败
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //下面是重入锁的处理
                //重入次数越界
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //直接更新锁状态
                setState(c + acquires);
                return true;
            }
    		//这种情况代表当前没有线程拥有锁
            if (writerShouldBlock() //判断写线程是否应该阻塞
                || !compareAndSetState(c, c + acquires)//cas更新锁
               )
                return false;
    		//获取锁成功,设置独占线程 
            setExclusiveOwnerThread(current);
            return true;
        }

总结上述源码的流程:

1.判断当前是否有线程持有锁,如果持有锁,继续判断

  • 判断持有的是否是读锁,如果是读锁,则直接获取失败(读写互斥,且读锁不能升级)
  • 判断持有写锁的线程是否是当前线程,不是则获取失败,是则更新state

2.当没有线程持有锁时,通过writerShouldBlock判断是否阻塞

非公平锁实现

永远返回false

java 复制代码
final boolean writerShouldBlock() {
            return false; // writers can always barge
        }

公平锁实现

java 复制代码
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        public final boolean hasQueuedPredecessors() {
            Thread first = null; Node h, s;
            if ((h = head) != null && ((s = h.next) == null ||
                                   (first = s.waiter) == null ||
                                   s.prev == null))
            first = getFirstQueuedThread(); // retry via getFirstQueuedThread
            return first != null && first != Thread.currentThread();
        }
  1. 通过CAS获取锁

释放锁流程

java 复制代码
        public void unlock() {
            sync.release(1);
        }
        public final boolean release(int arg) {
            if (tryRelease(arg)) {
                signalNext(head);
                return true;
            }
            return false;
        }

tryRelease的源码如下:

java 复制代码
        protected final boolean tryRelease(int releases) {
            //判断是否是当前线程持有的
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            //如果完全释放了,独占线程设置为null
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }
相关推荐
q5431470872 小时前
Java进阶总结——集合
java·开发语言
啥咕啦呛2 小时前
java打卡学习5:java基础学习
java·开发语言·学习
Lyyaoo.2 小时前
【JAVA基础面经】JAVA的面向对象特性
java·开发语言·windows
浮游本尊2 小时前
Java学习第37天 - 领域驱动设计(DDD)与 CQRS 实战
java
米糕闯编程2 小时前
xshell使用CentOS10 root用户登录,权限问题
java·linux
sxhcwgcy2 小时前
Python中的简单爬虫
java
woniu_maggie2 小时前
SAP CPI 开发RFC适配器的Integration Flow
后端
xiaoliuliu123452 小时前
Android Studio 2025 安装教程:详细步骤+自定义安装路径+SDK配置(附桌面快捷方式创建)
java·前端·数据库
老前端的功夫3 小时前
【Java从入门到入土】21:List三剑客:ArrayList、LinkedList、Vector的爱恨情仇
java·javascript·网络·python·list