ReentrantLock和AQS源码解读系列五

读写锁

这次要介绍一下ReentrantReadWriteLock,所谓的读写锁,其实也就是针对一些读多写少的情况,让读可以共享,写独占,提高效率。我们先来看个简单的例子,2个写线程,3个读线程,来观察下结果:

public class ReentrantReadWriteLockTest {

    private static StringBuilder stringBuilder = new StringBuilder();
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = reentrantReadWriteLock.readLock();
    private static Lock writeLock = reentrantReadWriteLock.writeLock();


    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            //写写互斥 写读互斥
            new Thread(() -> {
                while (true) {
                    try {
                        writeLock.lock();
                        System.out.println(Thread.currentThread().getName() + "开始写入:" + Thread.currentThread().getName());
                        stringBuilder.append(Thread.currentThread().getName());
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "写入完成");
                    } finally {

                        writeLock.unlock();
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                }

            }, "写线程" + i).start();
        }
        for (int i = 0; i < 3; i++) {
            //读读共享
            new Thread(() -> {
                while (true) {
                    try {
                        readLock.lock();
                        System.out.println(LocalDateTime.now() + "|" + Thread.currentThread().getName() + "开始读取:" + stringBuilder.toString());
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(LocalDateTime.now() + "|" + Thread.currentThread().getName() + "开始完成");
                    } finally {
                        readLock.unlock();
                    }

                }
            }, "读线程" + i).start();
        }
    }
}

结果就是写线程是独占的,一个个完成,而读线程可以共享,因此也提高了读线程的效率。

写线程0开始写入:写线程0
写线程0写入完成
写线程1开始写入:写线程1
写线程1写入完成
2020-01-10T11:46:08.290991400|读线程1开始读取:写线程0写线程1
2020-01-10T11:46:08.291991400|读线程0开始读取:写线程0写线程1
2020-01-10T11:46:08.291991400|读线程2开始读取:写线程0写线程1
2020-01-10T11:46:09.323050400|读线程0开始完成
2020-01-10T11:46:09.323050400|读线程2开始完成
2020-01-10T11:46:09.323050400|读线程1开始完成

读写锁的结构

接下来我们就来分析下这个读写锁的原理,首先看下他的结构,当然从源码里也能看到,但是这个图看起来比较直观,看上去很像很多东西,我们慢慢分析:
在这里插入图片描述

ReentrantReadWriteLock构造方法

其实可以看到,他内部有同步器,也可以有公平和非公平的锁,还有创建了读锁和写锁,其实他本身所有的功能就是考这些同步器实现的:

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

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

WriteLock

先来看看写锁,其实即是实现了Lock接口,具体的实现都是交给sync完成:
在这里插入图片描述
同步器sync是构造的时候外部传进来的:

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

ReadLock

跟写锁也类似,只是不支持条件同步,因为是共享的嘛,还要条件干嘛:

  public Condition newCondition() {
            throw new UnsupportedOperationException();
        }

NonfairSync

非公平同步器,是同步器的子类,只是两个方法不一样:

 static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            /* As a heuristic to avoid indefinite writer starvation,
             * block if the thread that momentarily appears to be head
             * of queue, if one exists, is a waiting writer.  This is
             * only a probabilistic effect since a new reader will not
             * block if there is a waiting writer behind other enabled
             * readers that have not yet drained from the queue.
             */
            return apparentlyFirstQueuedIsExclusive();
        }
    }

FairSync

公平嘛,当然要排队:

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();//看前面有没人排队
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }

Sync

接下去就是重点来了,核心基本都在这个同步器里,他把锁的状态用位来表示,int32位分成两半,高16位表示读锁已占资源,低16位表示写锁已占资源,我这里强调的是获取到的数值是已用的资源,不是还能用的资源,因为初始是0,用了一个+1,读写的上限都是65535个,所以读到的是已经用了多少个资源。如何获取这些资源呢,就用无符号按位移动和按位与来分别获取高16和低16位数据:

  		static final int SHARED_SHIFT   = 16;//位移的位数,为了获取高16位
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);//读锁1次添加的单位
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;//65535个
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//低16位都是1的遮罩,用于获取低16位

        /** Returns the number of shared holds represented in count. */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }//获取读锁状态,无符号右边移16个,即是读锁已用资源数
        /** Returns the number of exclusive holds represented in count. */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }//获取写锁状态,位运算与,直接提取低16位是写锁已用资源数

从网上弄了张图:
在这里插入图片描述

HoldCounter

接下去就是HoldCounter,一看就知道是持有的数量,也就是每个线程持有的资源数,为什么每个线程还能拿多个,因为可以重入,所以可以获取多个啦:

 /** 资源持有器 保存线程对应的锁资源的数量,缓存在cachedHoldCounter中
         * A counter for per-thread read hold counts.
         * Maintained as a ThreadLocal; cached in cachedHoldCounter.
         */
        static final class HoldCounter {
            int count;          // initially 0
            // Use id, not reference, to avoid garbage retention
            final long tid = LockSupport.getThreadId(Thread.currentThread());//会调用本地方法获取
        }
        //缓存最后一个成功获取读锁的线程的HoldCounter
         private transient HoldCounter cachedHoldCounter;

ThreadLocalHoldCounter

针对读锁共享的问题,如何将HoldCounter跟每个线程绑定呢,就可以用ThreadLocalThreadLocalHoldCounter是他的子类,而且限定了HoldCounter类型,这样每个线程就可以跟一个HoldCounter 绑定:

 /** 每个线程都对应单独的HoldCounter
         * ThreadLocal subclass. Easiest to explicitly define for sake
         * of deserialization mechanics.
         */
        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();//初始化默认值
            }
        }

        private transient ThreadLocalHoldCounter readHolds;//读资源持有器组,为每一个线程存储一个资源持有器
  		private transient Thread firstReader;//记录第一个读线程
        private transient int firstReaderHoldCount;//记录第一个读线程的持有资源数

writeLock.lock()

基本的结构讲完了,接下去我们要看主要的流程了,其他细节结合具体流程说,默认是非公平锁:

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

里面就是AQS的流程:

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

tryAcquire(int acquires)

但是这个方法重写了:

 @ReservedStackAccess //为了避免多线程栈溢出
        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();//已用的总的资源
            int w = exclusiveCount(c);//已用的写锁资源
            if (c != 0) {//资源有被用
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())//写资源为0,或者不是独占线程就返回失败
                    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;
        }

主要思路就是获取已用的总的锁资源和已经用的写锁资源:

  • 如果资源有被用了,写资源还没被用,说明有读线程在用,所以失败。

  • 如果资源有被用了,写资源有用,但是占有资源的线程不当前线程,重入不满足,所以失败。

  • 如果资源有被用了,写资源有用,占有资源的线程是当前线程,可重入,
    但是如果已用写资源+要申请的写资源总数超过限制了,返回失败
    否则就设置资源,返回成功

  • 如果资源还没被用过,写应该被阻塞的话或者写不阻塞但是资源修改失败,返回失败

  • 否则写不阻塞,修改资源成功,就设置独占线程,返回成功

可见这个主要是针对写锁来做限制的,这样要注意writerShouldBlock非公平锁就直接返回false,而公平锁需要看前面有没人在排队hasQueuedPredecessors(),这个前面的文章有讲过就不多说了。
剩下的都是以前讲过的独占锁的东西,就不说了:
在这里插入图片描述
可以参考下流程图:
在这里插入图片描述

writeLock.unlock();

接下去我们看怎么释放锁。

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

跟前面讲过的又一样,因为这个是模板方法啦。

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

tryRelease(int releases)

这个方法重写了,但是也比较简单,就是取判断减去释放的后写锁资源是不是0了,是的话才算释放成功,要去唤醒后继,否则可能有重入,还有资源没释放:

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

readLock.lock()

再来看看读锁:

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

就是共享锁的结构啊:

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

tryAcquireShared(int unused)

 @ReservedStackAccess
        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)) {//是否能修改成功
                if (r == 0) {//如果已读资源为0
                    firstReader = current;//设置当前线程为第一个读线程
                    firstReaderHoldCount = 1;//设置第一个读线程持有资源的数量1
                } else if (firstReader == current) {//如果第一个读线程就是当前线程,即可重入
                    firstReaderHoldCount++;//持有资源数+1
                } else {//不是第一个读线程
                    HoldCounter rh = cachedHoldCounter;//获得缓存资源持有器里
                    if (rh == null ||//如果这个缓存资源持有器为null,或者资源持有器的线程不是当前线程
                        rh.tid != LockSupport.getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();//初始化资源持有器并保存到缓存中
                    else if (rh.count == 0)//缓存资源持有器存在,且资源线程是当前线程,就是重入 如果持有资源为0
                        readHolds.set(rh);//将资源持有器放入读资源持有器组里
                    rh.count++;//持有资源数+1
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

首先会判断是否已用写资源,如果有,那其他线程就不能来,独占的,除非是写线程本身又申请了读资源才继续,这里可能就是所谓的写锁降级成读锁吧,我不太明白降级,难道写锁和读锁有级别之分么,为什么要搞的那么深奥,只就说写线程里还能获取读资源。如果没有,就继续。

然后进行是否要阻塞读线程的阻塞,公平锁会进行排队,非公平锁会对写线程有个优化,不让当前线程去抢占资源,让写线程优先获得独占资源,主要是这个方法readerShouldBlock中的apparentlyFirstQueuedIsExclusive

final boolean apparentlyFirstQueuedIsExclusive() {
//如果头结点和后续结点不为空,而且后续结点还是写线程独占的且不为空,就成功返回
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }
  1. 如果不阻塞满足个数条件,且修改成功,那就是获得了读资源:
    如果发现读资源没有被用过,就设置firstReaderfirstReaderHoldCount,也就是说有个缓存,提高性能。
    如果发现读资源被用过,而且firstReader是当前线程,表示重入,firstReaderHoldCount++
    如果上面的都不满足,说明不是第一个线程,看看是不是缓存cachedHoldCounter的线程:
    如果缓存是空,或者缓存里的线程不是当前线程,那就用读资源持有器集合readHolds创建一个读资源持有器,并且缓存给 cachedHoldCounter
    如果缓存不为空,且缓存里的线程就是当前线程,那就将缓存放入读资源持有器组readHolds里。
    持有资源数+1,返回true
  2. 如果阻塞条件不满足就会去执行完整的获取方法fullTryAcquireShared,等于说前面的是简易版的。

可以看下这个参考图:
在这里插入图片描述

fullTryAcquireShared(Thread current)

我们来看看完整的,其实也差不多,只是一个死循环,判断内容和上面差不多,只是针对CAS修改失败还会尝试,直到成功。注意如果阻塞了,也要分两种情况,一种如果是新来的,那真要阻塞,如果是重入,那就不需要:

   final int fullTryAcquireShared(Thread current) {
            /*
             * This code is in part redundant with that in
             * tryAcquireShared but is simpler overall by not
             * complicating tryAcquireShared with interactions between
             * retries and lazily reading hold counts.
             */
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // else we hold the exclusive lock; blocking here
                    // would cause deadlock.
                } else if (readerShouldBlock()) {//需要阻塞
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) {//重入
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null ||
                                rh.tid != LockSupport.getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();//删除,帮助GC,不然可能存在内存泄露
                            }
                        }
                        if (rh.count == 0)//如果持有器资源是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 {
                        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;
                }
            }
        }

readLock.unlock()

再来看看释放资源:

 public void unlock() {
            sync.releaseShared(1);
        }
 public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

tryReleaseShared(int unused)

主要是重写了tryReleaseShared(int unused)

  @ReservedStackAccess
        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {//如果第一个读线程是当前线程
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)//读资源为1就直接释放了
                    firstReader = null;
                else
                    firstReaderHoldCount--;//否则资源数-1
            } else {//不是第一个读线程
                HoldCounter rh = cachedHoldCounter;//获取缓存资源持有器
                if (rh == null ||//缓存为空或者当前线程不是缓存中的线程,缓存资源持有器只存最后一次成功获取锁的线程的数据
                    rh.tid != LockSupport.getThreadId(current))
                    rh = readHolds.get();//从读资源持有器组中获取
                int count = rh.count;
                 if (count <= 1) {
                    readHolds.remove();//删除资源持有器
                    if (count <= 0)//如果发现数量<=0,说明在释放不是当前线程的资源数
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;//读资源-1
                if (compareAndSetState(c, nextc))//如果是0表示释放全完,否则还有没释放的资源
                    // 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;
            }
        }

这个其实也很好理解,就是各种情况释放资源,如果放完了,才算真的释放完成,才会去唤醒后续的共享结点。

总结

线程获取写锁的同时还可以获取读锁

下面这段代码说明了,写的线程还可以读:
在这里插入图片描述
这样是可以的:
在这里插入图片描述
因为写是独占的,是不会有其他线程干扰,所以可以在里面继续获得读锁,而且不需要竞争,完了之后全部释放即可。

线程获取读的同时不能获取写锁

下面的代码就限制了,如果读里面获取写,就阻塞,然后就死锁了:
在这里插入图片描述
死锁:
在这里插入图片描述

参考

https://www.cnblogs.com/rain4j/p/10135283.html
https://www.jianshu.com/p/cd485e16456e

好了,今天就到这里了,希望对学习理解有帮助,大神看见勿喷,仅为自己的学习理解,能力有限,请多包涵。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值