ReentrantReadWriteLock源码分析

2019-06-07  本文已影响0人  无聊之园

ReentrantReadWriteLock顾名思义,三个意思:
1、可重入:和ReentrantLock一样,ReentrantLock重入一次,则AQS的status加+,而ReentrantReadWriteLock的读锁lock一次(不管重不重入),则status的高16位部分加1,
写锁lock一次,则status的低16位加1。
对于readLock,每个线程再ThreadLocal中维护了本线程重入的次数。

2、read锁。

3、write锁。

因为,只有一个status,但是读锁lock的时候,要检查是否有写锁已经lock了,同理,写锁lock的时候,也要确认是否有读锁lock,所以写锁和读锁在ReentrantReadWriteLock是分开标识的,status的高16位是读锁的status,低16位是写锁status。

因为多个读线程,可以重入,所以,怎么记录每个线程的重入次数呢?如果是一个线程则可以直接用AQS的status记录次数,但是有多个线程,所以,有一个ThreadLocal,记录每个线程自己的重入次数。

具体代码:

ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        reentrantReadWriteLock.writeLock().lock();

读写锁,其实是共用一个sync,sync是aqs的子类。

public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
abstract static class Sync extends AbstractQueuedSynchronizer {
 
      // 16位一个分片,总共两个分片
        static final int SHARED_SHIFT   = 16;
        // 所有读锁线程重入lock住的所有数量,读锁因为是高16位,所以,读锁次数加1,其实加这个高16位的1。
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        // 能锁住的最大的次数
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        // 写锁的掩码,可用用来,&运算,然后把高16位去掉,然后得到低16位
        // 也就是写锁,重入锁住的次数,因为是单个线程持久写锁,所以是单个线程的锁住次数
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
        //  读锁次数,右移16位,获取高16位对应的数
        /** 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; }

        // 这个内部类,是ThreadLocal维护的每个读锁线程的重入锁次数
        static final class HoldCounter {
            int count = 0;
            // Use id, not reference, to avoid garbage retention
            final long tid = getThreadId(Thread.currentThread());
        }
        // 这个ThreadLocal类维护上面的HoldCounter ,每个读锁线程纪录重入次数
        // 重写initialValue方法,是因为,不重写的话,必须先调用ThreadLocal.set(new HoldCounter) 才能调用ThreadLocal.get(),重写了initialValue之后,ThreadLocal.get()获取不到对象会调用initialValue初始化,也就是每次可以直接调用get方法获得HoldCounter对象,然后记录读锁次数
        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;

公平锁sync
可以看到:不管读还是写,都是调用hasQueuedPredecessors看队列是否还有元素在等待,如果有,则老老实实tryAcquired失败,添加到AQS队列的尾部

static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }
// 队列是否有元素
// head!=tail则说明队列有元素,head指的是正在获取到锁的资源,h.next是下一个获取到锁的资源
public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

非公平锁
可以看到:writerShouldBlock永远返回false,也就是说,写锁tryAcquire的时候,不管队列里有没有元素,直接cas替换status争抢资源。

然后,读锁,会调用AQS的apparentlyFirstQueuedIsExclusive方法,如果head(持有锁的线程)不为空,而且head的下一个元素是排他锁也就是写锁,则返回true,读锁乖乖进入队列尾部等待。

比如:队列头部正在持有锁的读线程下一个节点是写线程,然后大量的读请求lock过来了,然后这种情况,应该让写线程先执行,读线程进入队列尾部。

然后公平锁、非公平锁对比:
公平锁,如果队列里没有元素,readerShouldBlock查看堵塞队列有没有元素,所以返回false,的读锁直接cas争抢锁,反正可以并行,也就没有顺序之分。
如果队列里有元素(当有一个写锁进入了队列,那么以后的所有读锁都进入队列),之后,队列里的读和写是一样公平的,按顺序来,也就是说,写锁一定能够得到执行,并且,写锁是按顺序来执行,绝对的公平。

非公平锁,如果队列里没有元素,readerShouldBlock调用apparentlyFirstQueuedIsExclusive直接返回false,和公平锁一样,直接cas争抢锁,并行执行,没有顺序之分。
如果队列里有元素(当有一个写锁进入了队列,那么以后的所有读锁都进入队列),则apparentlyFirstQueuedIsExclusive会看队列的头部(持有锁的元素)的下一个将要获取锁的元素是否是写锁,如果是,则让它先获取锁,自己进入队列,否则,则跟它争抢锁。

这个看头部元素的下一个节点是否是写锁的机制,是为了,保证在非公平锁模式下,写锁也能得到执行,如果没有这个机制,可能,锁总是会被读锁cas争抢到,写锁就得不到执行了。

所以,好像,读锁在非公平锁和非公平锁方面,都没有什么取别:反正没有写锁的时候,都是争锁,并发执行,没有顺序。有写锁的时候,要不就按队列顺序等写锁执行完,要不就在写锁将要获取锁的时候,让它先获取。(但是,理论上,公平锁,写锁更不容易造成饥饿,因为,非公平锁的方式,队列里的读锁cas和非队列里的读锁竞争的时候,没cas成功,还要自旋cas)

但是在写锁方法:非公平和公平锁有很大不同,非公平锁,写锁的顺序完全没有顺序,谁cas到是谁的,因为writerShouldBlock永远返回false,让其直接去cas竞争,但是公平锁,能保证写锁的决定的顺序。

所以ReentrantReadWriteLock会存在这样一个问题:
如果读锁线程,长时间不释放,然后写锁在队列中等待,然后以后来的所有的读锁,都将进入队列中等待,造成,写锁和读锁都没法执行。可是按道理,读锁应该和读锁平行执行的。
针对这个问题:解决方案可以是哟个tryLock(long timeout, TimeUnit unit)这种超时机制,代替,lock。

JDK8,有一个ReentrantReadWriteLock的改进版StampedLock,StampedLock引入了一个stamp票据的概念,可以有乐观读,乐观读锁锁住之后,返回一个stamp票据,在读过数据之后,验证一下这个票据是否过期,如果在生成stamp和验证stamp过程中,这个数据改了,则stamp就会过期,则我们可以再自旋读数据或者用悲观读锁读数据。所以StampedLock乐观读可以很好解决ReentrantReadWriteLock的问题。

锁降级:获取写锁之后,可以再获取读锁,然后再释放写锁之后,读锁还存在,别人只可以读,不能修改数据,知道读锁释放了。这种机制可以保证,写锁释放后,别人再把数据改了,导致,使用到错误的数据。

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();
        }
    }
final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }

看读锁的lock方法

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

非公平锁

 protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            // 已经被写锁锁住了,则返回进入队列
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
          // 获取status的高16位,所有读锁进入次数
            int r = sharedCount(c);
            // readerShouldBlock就是上面说的非公平和公平锁的,控制是否进入队列堵塞
// 非公平锁,这里,直接看队列下一个元素是不是排他锁
// 满足的话,直接cas,替换status,获取资源锁
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                // 如果是第一个获取读锁的线程,则用firstReader 记录这个线程
                //firstReaderHoldCount 记录,第一个线程重入读锁次数
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    // 第一个线程重入读锁次数+1
                    firstReaderHoldCount++;
                } else {
                    // cachedHoldCounter初始化位null
                    // 为null的话,从ThreadLocal中取HoldCounter保存在cachedHoldCounter
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                  // 重入次数+1
                    rh.count++;
                }
                return 1;
            }
              // 获取锁失败,则进入这里
            return fullTryAcquireShared(current);
        }

不贴代码了,反正,重要内容前面都说了,对着代码看,大概逻辑都会明白。

上一篇下一篇

猜你喜欢

热点阅读