美文网首页
24.读写锁

24.读写锁

作者: 段段小胖砸 | 来源:发表于2021-11-09 17:47 被阅读0次

读写锁(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的根本差异所在。

相关文章

  • 24.读写锁

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

  • 读写锁和互斥锁 读写互斥锁,简称读写锁 mux sync.RWMutex Lock和Unlock分别对写锁进行锁定...

  • 线程同步(下)

    继上篇。这篇介绍的几种使用的较少。 读写锁 读写锁与互斥锁类似。不过读写锁允许更高的并行性。读写锁可以有三种状态:...

  • 可重入读写锁 ReentrantReadWriteLock

    读写锁分为读锁和写锁,多个线程获取读锁不互斥,读写锁、写写锁互斥。 输出

  • Java并发编程-读写锁(ReentrantReadWriteL

    章节目录 ReentrantReadWriteLock 特性 读写锁接口示例 读写锁的实现分析读写状态设计写锁的释...

  • 线程安全之读写锁

    相关API 初始化读写锁 释放读写锁 获取读锁 获取写锁 解锁 实例

  • ReadWriteLock读写锁

    1、引入ReadWriteLock读写锁 ReadWriteLock是JDK5中提供的读写分离锁。读写分离锁可以有...

  • 基于CAS的一些锁(5)- ReadWriteLock

    ReadWriteLock 读写锁。读写锁的概念其实就是共享锁和排他锁,读锁就是共享锁,写锁就是排他锁。 如何理解...

  • Go 语言的锁

    Go 语言提供两类锁: 互斥锁(Mutex)和读写锁(RWMutex)。其中读写锁(RWMutex)是基于互斥锁(...

  • 读写锁实现

    读写锁 ReentrantReadWriteLock可重入读写锁(实现ReadWriteLock接口) 使用:Re...

网友评论

      本文标题:24.读写锁

      本文链接:https://www.haomeiwen.com/subject/sjcyaltx.html