ReentrantReadWriteLock源码分析

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容