24.读写锁

读写锁(ReentrantReadWriteLock)就是读线程和读线程之间不互斥。
读读不互斥,读写互斥,写写互斥

1.类继承层次

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}
image.png

使用方法:

ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 
Lock readLock = readWriteLock.readLock(); 
readLock.lock(); 
// 进行读取操作 
readLock.unlock(); 

Lock writeLock = readWriteLock.writeLock(); 
writeLock.lock(); 
// 进行写操作 
writeLock.unlock();

也就是说,当使用 ReadWriteLock 的时候,并不是直接使用,而是获得其内部的读锁和写锁,然后分别调用lock/unlock

2.基本原理

从下面的构造方法可以看出,readerLock和writerLock实际共用同一个sync对象。sync对象同互斥锁一样,分为非公平和公平两种策略,并继承自AQS。

 public ReentrantReadWriteLock() {
     this(false);
  }

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

state:同互斥锁一样,读写锁也是用state变量来表示锁状态的。只是state变量在这里的含义和互斥锁完全不同。在内部类Sync中,对state变量进行了重新定义.也就是把 state 变量拆成两半,低16位,用来记录写锁。但同一时间既然只能有一个线程写,为什么还需要16位呢?这是因为一个写线程可能多次重入。例如,低16位的值等于5,表示一个写线程重入了5次。高16位,用来“读”锁。例如,高16位的值等于5,既可以表示5个读线程都拿到了该锁;也可以表示一个读线程重入了5次。

为什么要把一个int类型变量拆成两半,而不是用两个int型变量分别表示读锁和写锁的状态呢?

  • 这是因为无法用一次CAS 同时操作两个int变量,所以用了一个int型的高16位和低16位分别表示读锁和写锁的状态
  • 当state=0时,说明既没有线程持有读锁,也没有线程持有写锁;当state != 0时,要么有线程持有读锁,要么有线程持有写锁,两者不能同时成立,因为读和写互斥。这时再进一步通过sharedCount(state)和exclusiveCount(state)判断到底是读线程还是写线程持有了该锁。

3.ReadLock和WriteLock

在ReentrantReadWriteLock的两个内部类ReadLock和WriteLock中,是如何使用state变量的。

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

public static class WriteLock implements Lock, java.io.Serializable { 
    // ... 
    public void lock() { 
        sync.acquire(1); 
    }
    public void unlock() { 
        sync.release(1); 
    }
    // ... 
}

acquire/release、acquireShared/releaseShared 是AQS里面的两对模板方法。互斥锁和读写锁的写锁都是基于acquire/release模板方法来实现的。读写锁的读锁是基于acquireShared/releaseShared这对模板方法来实现的。

  1. 读锁的公平实现:Sync.tryAccquireShared()+FairSync中的两个重写的子方法。
  2. 读锁的非公平实现:Sync.tryAccquireShared()+NonfairSync中的两个重写的子方法。
  3. 写锁的公平实现:Sync.tryAccquire()+FairSync中的两个重写的子方法。
  4. 写锁的非公平实现:Sync.tryAccquire()+NonfairSync中的两个重写的子方法。
static final class NonfairSync extends Sync { 
    // 写线程抢锁的时候是否应该阻塞 
    final boolean writerShouldBlock() { 
        // 写线程在抢锁之前永远不被阻塞,非公平锁 
        return false; 
    }
    // 读线程抢锁的时候是否应该阻塞 
    final boolean readerShouldBlock() { 
        // 读线程抢锁的时候,当队列中第一个元素是写线程的时候要阻塞 
        return apparentlyFirstQueuedIsExclusive(); 
} }

static final class FairSync extends Sync { 
    // 写线程抢锁的时候是否应该阻塞 
    final boolean writerShouldBlock() { 
      // 写线程在抢锁之前,如果队列中有其他线程在排队,则阻塞。公平锁 
       return hasQueuedPredecessors(); 
    }
    // 读线程抢锁的时候是否应该阻塞 
    final boolean readerShouldBlock() { 
        // 读线程在抢锁之前,如果队列中有其他线程在排队,阻塞。公平锁 
        return hasQueuedPredecessors(); 
} }

对于公平,比较容易理解,不论是读锁,还是写锁,只要队列中有其他线程在排队(排队等读锁,或者排队等写锁),就不能直接去抢锁,要排在队列尾部。

对于非公平,读锁和写锁的实现策略略有差异。
写线程能抢锁,前提是state=0,只有在没有其他线程持有读锁或写锁的情况下,它才有机会去抢锁。或者state != 0,但那个持有写锁的线程是它自己,再次重入。写线程是非公平的,即writerShouldBlock()方法一直返回false。
对于读线程,假设当前线程被读线程持有,然后其他读线程还非公平地一直去抢,可能导致写线程永远拿不到锁,所以对于读线程的非公平,要做一些“约束”。当发现队列的第1个元素是写线程的时候,读线程也要阻塞,不能直接去抢。即偏向写线程。

4.WriteLock

写锁是排它锁,实现类似互斥锁。
tryLock()实现分析(非阻塞)

image.png

lock()方法(阻塞)

image.png

tryLock和lock方法不区分公平/非公平。ReentrantReadWriteLock的FairSync 和 NonfairSync 区别是 writerShouldBlock()、readerShouldBlock()两个方法,是在抢锁的时候用到的。FairSync 中的writerShouldBlock()方法,其中是写线程是有优先级的

unlock()实现分析

image.png

5.ReadLock

trylock

       public boolean tryLock() {
            return this.sync.tryReadLock();
        }
        @ReservedStackAccess
        final boolean tryReadLock() {
            // 获取当前线程
            Thread current = Thread.currentThread();

            int c;
            int r;
            do {
                // 获取state值
                c = this.getState();
                // 如果是写线程占用锁或者当前线程不是排他线程,则抢锁失败
                if (exclusiveCount(c) != 0 && this.getExclusiveOwnerThread() != current) {
                    return false;
                }
                // 获取读锁state值
                r = sharedCount(c);
                // 如果获取锁的值达到极限,则抛异常
                if (r == 65535) {
                    throw new Error("Maximum lock count exceeded");
                }
              // 使用CAS设置读线程锁state值
            } while(!this.compareAndSetState(c, c + 65536));
          // 如果r=0,则当前线程就是第一个读线程
            if (r == 0) {
                this.firstReader = current;
                // 读线程个数为1
                this.firstReaderHoldCount = 1;
            // 如果写线程是当前线程
            } else if (this.firstReader == current) {
                // 如果第一个读线程就是当前线程,表示读线程重入读锁
                ++this.firstReaderHoldCount;
            } else {
            // 如果firstReader不是当前线程,则从ThreadLocal中获取当前线程的读锁 个数,并设置当前线程持有的读锁个数
                ReentrantReadWriteLock.Sync.HoldCounter rh = this.cachedHoldCounter;
                if (rh != null && rh.tid == LockSupport.getThreadId(current)) {
                    if (rh.count == 0) {
                        this.readHolds.set(rh);
                    }
                } else {
                    this.cachedHoldCounter = rh = (ReentrantReadWriteLock.Sync.HoldCounter)this.readHolds.get();
                }

                ++rh.count;
            }

            return true;
        }

lock

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

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

        @ReservedStackAccess
        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = this.getState();
            if (exclusiveCount(c) != 0 && this.getExclusiveOwnerThread() != current) {
                return -1;
            } else {
                int r = sharedCount(c);
                if (!this.readerShouldBlock() && r < 65535 && this.compareAndSetState(c, c + 65536)) {
                    if (r == 0) {
                        this.firstReader = current;
                        this.firstReaderHoldCount = 1;
                    } else if (this.firstReader == current) {
                        ++this.firstReaderHoldCount;
                    } else {
                        ReentrantReadWriteLock.Sync.HoldCounter rh = this.cachedHoldCounter;
                        if (rh != null && rh.tid == LockSupport.getThreadId(current)) {
                            if (rh.count == 0) {
                                this.readHolds.set(rh);
                            }
                        } else {
                            this.cachedHoldCounter = rh = (ReentrantReadWriteLock.Sync.HoldCounter)this.readHolds.get();
                        }

                        ++rh.count;
                    }

                    return 1;
                } else {
                    return this.fullTryAcquireShared(current);
                }
            }
        }

readerShouldBlock()在公平和非公平中实现。

unlock()实现分析

image.png

tryReleaseShared()的实现:

        Thread current = Thread.currentThread();
        ....
        @ReservedStackAccess
        protected final boolean tryReleaseShared(int unused) {
            
            int c;
            do {
                c = this.getState();
                nextc = c - 65536;
            } while(!this.compareAndSetState(c, nextc));

            return nextc == 0;
        }

因为读锁是共享锁,多个线程会同时持有读锁,所以对读锁的释放不能直接减1,而是需要通过一个for循环+CAS操作不断重试。这是读锁的tryReleaseShared和写锁tryRelease的根本差异所在。

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

推荐阅读更多精彩内容