Fork me on GitHub

ReentrantReadWriteLock原理分析

概述

看名称就知道,这是一个读写锁,看上去好像和ReentrantLock有点关系,其实和ReentrantLock没有啥关系,两个类之间不存在互相调用和继承的关系,既然有了ReentrantLock为啥还要搞一个ReentrantReadWriteLock,主要还是因为ReentrantLock一次只能让一个线程访问临界区,效率太低,所以就引入了一个新的锁,让读锁和写锁分开,保证读锁之间不互斥,读锁和写锁互斥,这样读的并发就会高很多,上面说把读锁和写锁分开其实这种说法并不是很准确,因为这两者在AQS中并没有分开,还是使用同一个阻塞队列,还是使用同一个state,下面就详细介绍一下这个东东的原理,本文会分析源码,文章会比较臃肿。

ReentrantReadWriteLock类结构图

从上面结构图可以看出,左边部分是ReentrantReadWriteLock,有两个主要的内部类ReadLock和WriteLock,分别实现了读锁和写锁,右边部分是AQS,中间通过Sync连接,Sync有两个子类,FairSync和NonfairSync,分别实现了公平锁和非公平锁,那下面就把关键组件和其内部的核心方法分析一下。

ReadWriteLock接口

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

里面有两个方法,分别获取读锁和写锁的Lock对象,因为ReadLock和WriteLock都实现了Lock接口,在上面的结构图中没有画出来。

ReentrantReadWriteLock构造方法

/** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
    this(false);
}

/** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

Sync类介绍

Sync类中属性介绍

//位数
static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
//读锁和写锁最大数量
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;   
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//记录最后一个读锁的重入锁情况,至于HoldCounter,下面会分析
private transient HoldCounter cachedHoldCounter;
//下面会分析
private transient ThreadLocalHoldCounter readHolds;
//记录第一个读锁线程,第一个线程不用放在readHolds中,直接放在这两个变量中
private transient Thread firstReader = null;
//记录第一个读线程重入锁数量,这两个变量的主要作用是当只有一个读锁的时候,不用频繁访问ThreadLocal,毕竟通过
//ThreadLocal获取没有直接这样快
private transient int firstReaderHoldCount;

AQS是通过CAS修改state字段来加锁的,对于ReentrantLock这没有问题,因为对于ReetrantLock当state = 0的时候表示没有线程占用锁,state > 0表示有线程占用锁,但ReentrantReadWriteLock却不能这样使用,因为这是读写锁,即需要表示读锁的加锁状态,又需要表示写锁的加锁状态,所以就把这个32位int类型的字段给拆分了,前面16位表示读锁,后面16位表示写锁,所以读锁和写锁的最大数量都是65535个。

上面的几个变量就是为了辅助位运算的,具体使用后面会看到。

Sync内部类介绍

 static final class HoldCounter {
            int count = 0;
            // Use id, not reference, to avoid garbage retention
            final long tid = getThreadId(Thread.currentThread());
        }
 static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

这两个类非常重要,先说一下是干什么用的,由于ReentrantReadWriteLock是重入锁,所以需要记录每个线程重入锁数量,因为在释放锁的时候很重要,当某个线程占用的锁为0了,才说明该线程不再占用锁。对于写锁来说很简单,直接使用state记录就可以,因为只能有一个线程竞争到写锁,低16位的值就是写锁的重入数量,但是对于读锁就不行了,高16位只能记录读锁的总数,没法记录具体每个线程有多少个读锁,所以就有了上面的类,下面就具体介绍一下这两个类。

  1. HoldCounter类,这个类中的count就是记录该线程重入锁的数量,tid就是该线程的id,但是如果不做一些特殊的处理,那这个类的对象每个线程都共享,那最后count记录的还是锁的总数,那怎么让每个线程都拥有一个HoldCounter对象呢?很简单使用ThreadLocal就可以实现,不熟悉ThreadLocal的,可以参考一下我的另一篇文章:ThreadLocal原理分析
  2. ThreadLocalHoldCounter这个类继承了ThreadLocal类,并且重写了initialValue方法,并且return的是new HoldCounter(),这个可以保证每个线程所拥有的都是不同的HoldCounter对象。

Sync重要方法分析

构造方法

Sync() {
            readHolds = new ThreadLocalHoldCounter();
            setState(getState()); // ensures visibility of readHolds
        }

在构造方法中,初始化了ThreadLocal的子类。

 获取读锁数量

static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

获取写锁数量

static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

尝试释放写锁

protected final boolean tryRelease(int releases) {
            //持有独占锁的线程是不是当前线程,如果不是抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            //free = 0说明重入锁都释放完了
            if (free)
                setExclusiveOwnerThread(null);
            //由于写锁占用的是低16位,可以直接这样修改
            setState(nextc);
            return free;
        }

尝试释放读锁

protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            //当前线程是不是第一个获取读锁的线程
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                //清楚第一个读线程相关的信息

                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                //拿到最后一个读线程重入锁相关的信息
                HoldCounter rh = cachedHoldCounter;
                //如果最后一个读线程重入锁信息为null,这个判断主要是为了防止后面执行rh.tid报空指针异常
                //或者当前线程不是最后一个读线程
                if (rh == null || rh.tid != getThreadId(current))
                    //从ThreadLocal中重新拿当前线程的重入锁信息
                    rh = readHolds.get();
                //当前线程的重入锁数量
                int count = rh.count;
                //如果count<=1,那这个锁释放之后,当前线程就不再拥有锁,所以把ThreadLocalMap中的当前ThreadLocal清除掉,防止发生内存泄漏
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                //如果是最后一个读线程,并且重入锁数量大于1,执行减1操作
                --rh.count;
            }
            //使用死循环,确保通过CAS释放锁成功
            for (;;) {
                int c = getState();
                //这里要主要,因为读锁在state中是高16位,减1就相当于减去2^16
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    //这里返回的很有意思,返回的并不是当前线程有没有释放读锁成功,而是当前系统中还有没有读
//如果没有读锁了,直接返回true,如果没有读锁存在了采取激活阻塞队列中的线程 return nextc == 0; } }

这个方法的返回值是判断系统中还有没有读锁,只有没有读锁了才返回true,这个时候如果阻塞队列中第二个节点是要获取写锁的线程,则可以直接激活。这样做可以防止读锁和写锁同时存在。

获取写锁

protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            //获取写锁的数量
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                //c != 0,w == 0说明又读锁存在,直接返回false
                //后一个条件说明有另一个线程占用写锁
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //超过最大值,抛出异常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            //这里的writerShouuldBlock是为了实现公平锁和非公平锁使用的,对于公平锁来说,如果
            //队列中还有别的线程在等待锁,直接返回false,这个方法在介绍FairSync的时候在详细介绍
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

获取读锁

protected final int tryAcquireShared(int unused) {

            Thread current = Thread.currentThread();
            int c = getState();
            //如果有写锁存在,并且写锁还不是自己这个线程,这里就比较有意思了,正常应该是只要存在写锁,就应该直接返回-1,
            //这里又加了一个条件,也就是说如果写锁存在,而且是当前线程的时候,就可以加读锁,这就是所谓的锁降级,后面再详细介绍
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            //仍然需要判断是公平锁和非公平锁
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //加锁成功,r == 0,说明是第一个读锁
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    //不是第一个读锁的情况
                    HoldCounter rh = cachedHoldCounter;
                    //如果也不是最后一个读锁
                    if (rh == null || rh.tid != getThreadId(current))
                        //从新从ThreadLocalMap中获取
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            //以上都没有成功或者失败,就自旋重试
            return fullTryAcquireShared(current);
        }

尝试获取写锁

//这个方法是尝试获取写锁,但是没有区分公平和非公平,就是试试,行就行,不行就直接返回false
final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {
                int w = exclusiveCount(c);
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
            }
            if (!compareAndSetState(c, c + 1))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

尝试获取读锁

//这个方法也是没有区分公平和非公平,就是尝试着获取读锁,成功返回true,不成功返回false,最后不会加入阻塞队列
final boolean tryReadLock() {
            Thread current = Thread.currentThread();
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)
                    return false;
                int r = sharedCount(c);
                if (r == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                    }
                    return true;
                }
            }
        }

太多了,脑壳痛。。。

        

锁降级

定义:在读写锁中先获取写锁,之后获取读锁,之后释放写锁,最后释放读锁,如果读锁在写锁前释放,那这个读锁加的就毫无意义。

以上方法中我想说明一下锁降级,在#tryAcquireShared()方法中,如下

  if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;

  这段代码在上面的注释中也有说明,这段代码的意思是当前时刻存在写锁,而且如果自己这个获取读锁的线程和当前持有写锁的线程是同一个线程的话,就不会返回-1,也就是说可以继续获取读锁,这么做有什么好处,看下面一段代码:

 class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 
   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
        // Must release read lock before acquiring write lock
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        try {
          // Recheck state because another thread might have
          // acquired write lock and changed state before we did.
          if (!cacheValid) {
            data = ...
            cacheValid = true;
          }
          // Downgrade by acquiring read lock before releasing write lock
          rwl.readLock().lock();
        } finally {
          rwl.writeLock().unlock(); // Unlock write, still hold read
        }
     }
 
     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
 }

这段代码是官方给的一个例子,这段代码的意思是在写锁代码段中修改data的值,之后释放写锁,但是释放写锁之后依然要使用data的值,就是还需要读取data的值,上面代码使用了锁降级的写法,如果不使用锁降级会有如下几种情况:

  1. 加写锁,修改data,释放写锁,不加读锁直接使用data的值,这时候由于写锁已经释放,别的线程可以竞争到写锁,修改data,那use(data)中的data就可能和自己修改的data不一致,因为在执行use(data)之前可能被别的线程修改了,这种写法非常不推荐,因为这里读取的data可能是新值,也可能是旧值,模棱两可。
  2. 加写锁,修改data,不释放写锁,直到use(data)执行完之后再释放写锁,这样确实可以保证数据一致性,但是如果use(data)执行2s,那相当于所有的别的线程的读和写都要等待2s,这样也不推荐,效率太低,每个线程修改都等待几秒,多线程修改的时候时间浪费就很可观了。
  3. 加写锁,修改data,释放写锁,之后加读锁,执行use(data),释放读锁,这样可以保证数据是最新的数据,但是这样需要重新竞争读锁,又多浪费了一点时间,勉强算是这三种情况当中最好的情况。

综合上面的三种情况,就可以看出锁降级是最优的选择。

 NonfairSync代码分析

static final class NonfairSync extends Sync {
    //在非公平锁中,写锁的获取不会管阻塞队列中有没有别的等待写锁的线程,直接去竞争
    final boolean writerShouldBlock() {
                return false; // writers can always barge
        }
    //即便是在非公平锁中,读锁也要让着写锁
    final boolean readerShouldBlock() {
                return apparentlyFirstQueuedIsExclusive();
        }
}

进入#apparentlyFirstQueuedIsExclusive()方法

    final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }

这段代码的作用就是判断阻塞队列第二个节点是不是等待获取写锁的线程,如果是,上面的#readerShouldBlock()就返回true,那获取读锁的线程就应该等待,放入等待队列,如果当前有很多读锁线程正在执行,那处于阻塞队列第二个节点的线程就一直等待,直到所有的读锁全部释放,之后就开始执行写锁

fairSync代码分析

static final class FairSync extends Sync {
 
        //这个和ReentrantLock的公平锁写法一样,都是判断当前队列中是否有正在等待的队列
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        //和上面方法的逻辑一样,都是看阻塞队列中是否有等待线程存在,如果有,就阻塞
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }

ReadLock代码分析

构造方法

        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }

这个lock.sync就是ReentrantReadWriteLock构造方法决定的,上面有介绍。

加锁方法

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

进入acquireShare(1)

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

这里的#tryAcquireShared()方法在上面介绍Sync类的时候有介绍,小于0说明加锁失败,下面那个方式是AQS中的方法,是把当前线程放入阻塞队列使用的,之前的文章分析过,就不重复分析了。

可中断加锁方法

      public void lockInterruptibly() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }

具体解释就是,在获取锁失败之后等待过程中,如果当前线程被中断,抛出异常。

尝试加锁

     public boolean tryLock() {
            return sync.tryReadLock();
        }

这个就是上面介绍Sync中介绍的,这个是尝试加锁,如果加锁失败,不会把当前线程放入阻塞队列等待,直接返回失败,而且该方法不区分公平锁和非公平锁,直接就去竞争,很粗暴。

可中断并且带有超时等待的加锁

        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
        }

这个方法虽然也叫tryLock,但是和上面的方法不同,上面的方法是要求在很短的时间内获取锁,如果没有获取就返回失败,而且不区分公平锁和非公平锁,这个方法不同,可以自己设置超时时间,如果获取锁失败,会把当前线程放入等待队列,如果等待时间超过设定的时间,加锁失败,这个方法还区分公平锁和非公平锁,并且这个方法可中断。

解锁

    public void unlock() {
            sync.releaseShared(1);
        }

在Sync中分析过

Condition

    public Condition newCondition() {
            throw new UnsupportedOperationException();
        }

读锁不支持condition,直接抛出异常

WriteLock代码分析

构造方法

protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }

同上

加锁

     public void lock() {
            sync.acquire(1);
        }

进入#acquire(1)

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

这里的tryAcquire就是在Sync中分析的,尝试获取写锁。

可中断加锁方法

       public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }

尝试加写锁

 public boolean tryLock( ) {
            return sync.tryWriteLock();
        }

如果失败,直接返回,不区别公平和非公平

可中断并且带有超时等待的加锁

public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }

同上

Condition

 public Condition newCondition() {
            return sync.newCondition();
        }

写锁支持condition

总结

相比于ReentrantLock,读写锁的实现复杂一些,里面有很多的点很巧妙,比如下面几点:

  • 将state拆分,高16位表示读锁状态,低16位表示写锁状态
  • 使用ThreadLocal封装HoldCounter对象,保证每个线程记录自己的重入锁数量
  • 使用锁降级,提高效率
  • 读锁不互斥,读锁和写锁互斥
  • 将首个持有读锁的线程单独保存,而不是放入ThreadLocal中,这样在只有一个读线程的场景中提高效率
  • 保证写锁优先,如果当前读锁正持有锁,在新的线程获取读锁的时候,先看一下阻塞队列第二个节点是不是写锁线程,如果是就阻塞,防止写锁饥饿
  • 公平性,无论写锁还是读锁都支持公平锁

 

参考:

【死磕 Java 并发】—– J.U.C 之读写锁:ReentrantReadWriteLock

图解ReentrantReadWriteLock实现分析

Read-write lock ReentrantReadWriteLock lock downgrade

 

posted @ 2020-09-09 09:46  猿起缘灭  阅读(501)  评论(0编辑  收藏  举报