JUC源码解析-ReentrantReadWriteLock

本文详细探讨了JUC包中的ReentrantReadWriteLock,指出其读锁为共享锁,写锁为独占锁,支持读写、写写、读读重入,但不支持写读升级。ReentrantReadWriteLock具有公平与非公平模式,使用AQS的state字段的高低16位分别记录读写锁数量,并通过ThreadLocal优化读锁记录。文章详细阐述了读锁和写锁的获取与释放过程,以及writerShouldBlock和readerShouldBlock方法的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ReentrantLock是独占锁,只允许一个线程执行;CountDownLatch,Semaphore等是共享锁;它们分别利用了AQS的独占与共享功能;那么如果在读操作远多于写操作的情况下该如何选择?读写锁,之前的文章中介绍了如何自己实现一个读写锁,还实现了重入功能,读读,写写,读写,写读四种重入。现在来看看JUC包下的ReentrantReadWriteLock的实现。
先来大致了解下ReentrantReadWriteLock:

  • 读锁是个共享锁,写锁是个独占锁。读锁同时被多个线程获取,写锁只能被一个线程获取。读锁与写锁不能同时存在。
  • 一个线程可以多次重复获取读锁和写锁
  • 锁降级:获取写锁的线程又获取了读锁,之后释放写锁,就变成了一个读锁,这就是锁降级。
  • 锁升级:不支持升级。获取读锁的线程去获取写锁的话会造成死锁。
  • 重入数:AQS是state,前16位表示读锁的数量,后16位表示写锁的数量
  • 公平与非公平两种模式

AQS维护了一个int值,表示同步状态;对于ReentrantLock,state会在0与1之间变化,1表示已被占有后续线程入队列等待,0表示free。对于CountDownLatch,会先将state赋予个大于0的值,在该值变为0后唤醒等待队列中的线程。那么如何用它来即表示读锁又表示写锁呢?读锁我们是允许多个线程同步运行的,我们还允许重入,那么拿什么来记录每个线程读锁的重入数?

针对上面两个问题,对于同步状态status,高16位表示所有线程持有的读锁总数,低16位为一个线程的写锁总数,包括重入。

    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;
        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;

        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
       
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

采用ThreadLocal来记录每个线程锁持有的读锁数目。

        static final class HoldCounter {
            int count = 0; // 该线程所持有的读锁数量
            // Use id, not reference, to avoid garbage retention
            final long tid = getThreadId(Thread.currentThread());
        }

        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

        private transient ThreadLocalHoldCounter readHolds;
        private transient HoldCounter cachedHoldCounter;
        private transient Thread firstReader = null;
        private transient int firstReaderHoldCount;
  • HoldCounter 静态内部类用来记录一个线程的读锁重入数,以及id;
  • ThreadLocalHoldCounter继承了ThreadLocal,实现了initialValue方法,作用是在没有set前调用get的话initialValue会被调用,HoldCounter对象会被存储到Entry里,并返回它。变量名为readHolds。因为读锁是共享的,我们利用同步状态的高16位来记录总数,用threadlocal来记录每个线程所持有的读锁数目。对于写锁来说它是独占锁,低16位代表的就是当前线程持有的写锁数目。
  • cachedHoldCounter:它是一种优化的手段,为了避免频繁的调用ThreadLocalHoldCounter的读取,更改操作,于是缓存最新一个成功获取锁的线程的HoldCounter,意思是当一个线程需要记录值的时候会先检查自己是否是cachedHoldCounter中缓存的那个线程,是的话就不用再从readHolds中获取了,减少对ThreadLocal的操作。
  • firstReader 与firstReaderHoldCount:代表首个获取读锁的线程与其所持有的读锁数,该读锁数不会存储进readHolds,这是种优化,例如获取读锁执行的线程执行然后释放下一个再获取执行释放,这样一个接一个的情况。

同ReentrantLock一样内部实现了非公平与公平两种同步器:NonfairSync &FairSync ,继承自同一同步器Sync。

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
    }

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }

只定义了writerShouldBlock & readerShouldBlock两种方法,它们作用在获取锁的过程中,决定当前线程是否该阻塞。

构造器:

    public ReentrantReadWriteLock() {
        this(false);
    }

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

读写锁

private final ReentrantReadWriteLock.ReadLock readerLock;

private final ReentrantReadWriteLock.WriteLock writerLock;

public static class ReadLock implements Lock, java.io.Serializable {
        private final Sync sync;

        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
    ......

public static class WriteLock implements Lock, java.io.Serializable {
        private final Sync sync;

        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }

获取读写锁

ReentrantReadWriteLock继承了ReadWriteLock,这两个方法分别返回读锁与写锁。ReentrantReadWriteLock内部实现了两个类:ReadLock&WriteLock分别实现读锁与写锁。

public interface ReadWriteLock {
    Lock readLock();

    Lock writeLock();
}
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

一 读锁

读锁的lock

public static class ReadLock implements Lock, java.io.Serializable {  
      public void lock() {
            sync.acquireShared(1);
        }

定位到AQS的acquireShared,该方法之前介绍过。

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

来看看Sync重写的tryAcquireShared方法

        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            //写锁,且写线程非本线程,返回-1,对于共享锁这代表获取锁失败
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c); // 读锁数量
//readerShouldBlock返回true代表读锁的获取应该等待,
//cas将读锁个数加一
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
// 该线程是第一个读线程,firstReader,firstReaderHoldCount
// 用于只有一个线程操作锁的情况,避免频繁对ThreadLocal的操作
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) { 
                    firstReaderHoldCount++;
                } else {
//这里就能看出cachedHoldCounter的作用,它存储的是上一个获取锁的线程的HoldCount
//该变量也是种优化手段,总之是得到当前线程的HoldCounter 将其count++
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1; // 代表获取到读锁
            }
// 若上面的if判断返回false,如head.next为独占节点,
//cas增加读锁个数失败,就会运行到这。
//该方法是tryAcquireShared的升级版
            return fullTryAcquireShared(current);
        }

readerShouldBlock()方法:返回true并不代表当前线程就不能获取读锁,在tryAcquireShared中返回true会调用fullyTryAcquireShared,在该方法中对于readerShouldBlock返回true的处理是:在该线程非重入获取读锁,则返回-1,代表不允许获取读锁,也就是 写读重入或读读重入 的优先级大于 写饥饿操作与公平规则,这里写饥饿指的是非公平锁的readerShouldBlock实现。
公平模式下,根据队列中当前线程之前有没有等待的线程来决定。

//head.next存在且不是本节点返回true
final boolean readerShouldBlock() {
    return hasQueuedPredecessors();
}

非公平模式

final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();
}

调用apparentlyFirstQueuedIsExclusive()

    final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }
  • 返回true代表等待队列head.next节点是等待写锁的线程,该方法的目的是不让写锁一直等待下去;比如在上一篇自己实现的读写锁中,通过增加一个写请求变量来防止写饥饿,让写锁的优先级高于读锁。这里有相似的目的。
  • 这个方法是不可靠的,因为在检测过程中队列结构是在变化的,;但是我们并不依赖于它的准确表达,它更多是一种探测,一种优化,我们希望它来防止写锁的饥饿;而且并不是该方法返回了true,线程就会被放入阻塞队列退出竞争,来看fullTryAcquireShared的逻辑。
        final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                //非本线程持有写锁,返回-1,代表无法获取读锁
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // else we hold the exclusive lock; blocking here
                    // would cause deadlock.
//readerShouldBlock返回true,但也只有在该线程不是重入获取读锁的情况下,返回-1阻塞该线程
                } else if (readerShouldBlock()) {
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) { //若是重入便可以获取读锁
                        // assert firstReaderHoldCount > 0;
                    } else {
                     //获取该线程的HoldCounter,若其count大于0代表是重入
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0) //count为0删除ThreadLocal中的缓存
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                if (sharedCount(c) == MAX_COUNT) //超过最大值
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) { //读锁数加一
                    if (sharedCount(c) == 0) { //代表该线程是第一个获取读锁的线程
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        //获取当前线程的HoldCounter,将其count加一
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

之前说fullTryAcquireShared是tryAcquireShared的升级版,它处理了CAS失败和readerShouldBlock返回true的情况;

  1. 先检查写锁是否被其他线程占用;
  2. 调用readerShouldBlock检查当前线程是否应该进入等待队列,返回true也不代表该线程要进入等待队列,我们看它的处理逻辑:只有在当前线程没有持有读锁的情况下才阻塞等待。为什么?因为持有读锁的线程不能阻塞,否则会造成所有要获取写锁的线程无法获取到写锁,全都阻塞在同步队列中。
  3. 之后便是CAS,成功便更改HoldCounter值返回1,代表获取读锁成功,否则循环再次检查,总之不能轻易的将线程放入等待队列,容易造成死锁。

总结就是fullTryAcquireShared,采用for循环方式让线程不断判断与尝试,且只有在一种情况下才会将线程放入队列中阻塞等待:readerShouldBlock返回true且该线程并不持有读锁。

释放读锁

当前线程将读锁全部释放完才会返回true。

        public void unlock() {
            sync.releaseShared(1);
        }

定位到AQS的releaseShared,之前介绍过。

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

来看看同步器Syn实现的tryReleaseShared

        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            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;
            }
        }

1.如果是firstReader ,就对它及firstReader进行修改;2.如果不是,就对readHolds进行修改;3. 自旋CAS修改status
返回true代表status == 0,表示既没有读锁也没有写锁。

 

tryLock方法

tryLock只是线程获取锁的一次尝试,失败也不用放入队列中阻塞等待,所以才不考虑readerShouldBlock。

        public boolean tryLock() {
            return sync.tryReadLock();
        }

调用了同步器Syn的tryReadLock

        final boolean tryReadLock() {
            Thread current = Thread.currentThread();
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)
                    return false;
                int r = sharedCount(c);
                if (r == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                    }
                    return true;
                }
            }
        }

 

二, 写锁

获取写锁。1,持有写锁的线程可以再次获取写锁。2,持有写锁的线程可以获取读锁

        public void lock() {
            sync.acquire(1);
        }

定位到AQS的acquire中,AQS文章里介绍过。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

来看看同步器Syn重写的tryAcquire方法

        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 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 != 0下分两种 1. w == 0 代表有读锁;2. w != 0 && current != getExclusiveOwnerThread() 代表有其他写锁;以上两种返回false,线程要进入等待队列。否则就设置status值,返回true,获取写锁成功。
二 :c == 0情况下要看writerShouldBlock的情况,返回false就会去CAS更改同步状态,成功就将AOS里的exclusiveOwnerThread设置位当前线程,最后返回true;

注:情况一用setState更改同步状态,情况二用compareAndSetState?情况一执行到setState这步说明当前线程已持有写锁,是在重入,其他线程都会被排斥不同担心线程安全问题,所以setState就可以,同步状态status是volatile的。情况二里当前即无读锁也无写锁,当前线程始于其他线程在竞争,所以要利用CAS来保证原子性。
持有读锁线程不能申请写锁,从tryAcquire可以看出持有读锁线程要获取写锁一定会返回false,也就是会被放入队列中等待写锁,但是它持有的读锁将不会被释放,也就是state != 0 && exclusiveCount(state) == 0为true,所有获取写锁的线程都将永远不可能获得写锁。

来看看writerShouldBlock方法,不同于读锁,对于写锁的获取若writerShouldBlock返回true,则该线程无法获取写锁。
非公平模式:直接返回false,即写优先级高;

        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }

公平模式:head.next节点存在且不是当前线程。

        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }

释放写锁

        public void unlock() {
            sync.release(1);
        }

定位到AQS中的release

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

来看看同步器Syn重写的tryRelease

        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

注意返回值:返回true代表写锁个数为0,也就是当前无写线程;

tryLock方法

        public boolean tryLock( ) {
            return sync.tryWriteLock();
        }

定位到同步器Syn

        final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {
                int w = exclusiveCount(c);
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
            }
            if (!compareAndSetState(c, c + 1))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

尝试获取写锁,失败也不用放入队列等待,所以不用考虑writerShouldBlock情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值