美文网首页
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.读写锁

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