java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级

特点

和ReentrantLock一样,公平/非公平,可重入等概念可以看之前写过的这篇:
java并发编程ReentrantLock类和可重入锁概念,公平/非公平锁区别,可重入抛异常是否会释放锁
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第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();
     }
   }
 }

锁降级以后, 写锁并不会直接降成读锁, 不会随着读锁的释放而释放, 因此需要显式地释放写锁

锁降级的应用场景: 对于数据比较敏感, 需要在对数据修改以后, 获取到修改后的值, 并进行接下来的其它操作

2.用来提高某些集合的并发性能。当集合比较大,并且读比写频繁时,可以使用该类。下面是TreeMap使用ReentrantReadWriteLock进行封装成并发性能提高的一个例子:

class RWDictionary {
   private final Map<String, Data> m = new TreeMap<String, Data>();
   private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
   private final Lock r = rwl.readLock();
   private final Lock w = rwl.writeLock();

   public Data get(String key) {
     r.lock();
     try { return m.get(key); }
     finally { r.unlock(); }
   }
   public String[] allKeys() {
     r.lock();
     try { return m.keySet().toArray(); }
     finally { r.unlock(); }
   }
   public Data put(String key, Data value) {
     w.lock();
     try { return m.put(key, value); }
     finally { w.unlock(); }
   }
   public void clear() {
     w.lock();
     try { m.clear(); }
     finally { w.unlock(); }
   }
 }

上面这两个例子源码里的都有,可以自己去看

java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第2张图片

图解读写锁策略

ReentrantReadWriteLock的锁策略有两种,分为公平策略和非公平策略,区别在于随机和顺序获取,非公平吞吐量大,默认非公平。

每个线程中的 R(0)W(0)表示当前线程占用了多少读写锁。

java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第3张图片
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第4张图片
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第5张图片

java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第6张图片
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第7张图片
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第8张图片
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第9张图片
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第10张图片

源码分析

构造方法

ReentrantReadWriteLock有两个构造方法,和ReentrantLock一样,构造公平/非公平锁,默认非公平

public ReentrantReadWriteLock() {
        this(false);
    }

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

读写锁构造如下
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第11张图片
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第12张图片

Sync内部类

java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第13张图片

Sync继承了aqs,FairSync/NofairSync继承Sync

FairSync实现
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第14张图片
两个方法读写block都表示当有别的线程也在尝试获取锁时,是否应该阻塞。对于公平锁,hasQueuedPredecessors()方法表示前面是否有等待线程。一旦前面有等待线程,那么为了遵循公平,当前线程也就应该被挂起。

NonfairSync实现
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第15张图片
非公平模式下,写block直接返回false,说明不需要阻塞;而读block调用的方法表示:如果当前有一个写线程正在写,那么该读线程应该阻塞。

继承AQS的类都需要使用state变量代表某种资源,ReentrantReadWriteLock中的state代表了读锁的数量和写锁的持有与否,整个结构如下:
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第16张图片
可以看到state的高16位代表读锁的个数;低16位代表写锁的状态。

获取锁

读锁获取
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第17张图片
使用的是aqs的共享模式
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第18张图片
tryAcquireShared方法小于0时,那么会执行doAcquireShared方法将该线程加入到等待队列中。
Sync实现了tryAcquireShared方法,如下:

		protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. If write lock held by another thread, fail.
             * 2. Otherwise, this thread is eligible for
             *    lock wrt state, so ask if it should block
             *    because of queue policy. If not, try
             *    to grant by CASing state and updating count.
             *    Note that step does not check for reentrant
             *    acquires, which is postponed to full version
             *    to avoid having to check hold count in
             *    the more typical non-reentrant case.
             * 3. If step 2 fails either because thread
             *    apparently not eligible or CAS fails or count
             *    saturated, chain to version with full retry loop.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            //如果当前有写线程并且本线程不是写线程,不符合重入,失败
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            //得到读锁的个数
            int r = sharedCount(c);
            //如果读不应该阻塞并且读锁的个数小于最大值65535,并且可以成功更新状态值,成功
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //如果当前读锁为0
                if (r == 0) {
                    //第一个读线程就是当前线程
                    firstReader = current;
                    firstReaderHoldCount = 1;
                }
                //如果当前线程重入了,记录firstReaderHoldCount
                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 1;
            }
            //否则,循环尝试
            return fullTryAcquireShared(current);
        }

代码分为三步:

  1. 如果当前有写线程并且本线程不是写线程,那么失败,返回-1
  2. 否则,说明当前没有写线程或者本线程就是写线程(可重入),接下来判断是否应该读线程阻塞并且读锁的个数是否小于最小值,并且CAS成功使读锁+1,成功,返回1。其余的操作主要是用于计数的
  3. 如果2中失败了,失败的原因有三,第一是应该读线程应该阻塞;第二是因为读锁达到了上线;第三是因为CAS失败,有其他线程在并发更新state,那么会调动fullTryAcquireShared方法。
  final int fullTryAcquireShared(Thread current) {
           
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                //一旦有别的线程获得了写锁,返回-1,失败
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                } 
                //如果读线程需要阻塞
                else if (readerShouldBlock()) {
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    }
                    //说明有别的读线程占有了锁
                    else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                //如果读锁达到了最大值,抛出异常
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //如果成功更改状态,成功返回
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

在上面可以看到多次调用了readerShouldBlock方法,对于公平锁,只要队列中有线程在等待,那么将会返回true,也就意味着读线程需要阻塞;对于非公平锁,如果当前有线程获取了写锁,则返回true。一旦不阻塞,那么读线程将会有机会获得读锁。

写锁获取

java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第19张图片
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第20张图片
写锁使用的是AQS的独占模式。首先尝试获取锁,如果获取失败,那么将会把该线程加入到等待队列中。

Sync实现了tryAcquire方法用于尝试获取一把锁,如下:

protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
             //得到调用lock方法的当前线程
            Thread current = Thread.currentThread();
            int c = getState();
            //得到写锁的个数
            int w = exclusiveCount(c);
            //如果当前有写锁或者读锁
            if (c != 0) {
                // 如果写锁为0或者当前线程不是独占线程(不符合重入),返回false
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //如果写锁的个数超过了最大值,抛出异常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 写锁重入,返回true
                setState(c + acquires);
                return true;
            }
            //如果当前没有写锁或者读锁,如果写线程应该阻塞或者CAS失败,返回false
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            //否则将当前线程置为获得写锁的线程,返回true
            setExclusiveOwnerThread(current);
            return true;
        }

代码分为三步:

  1. 如果当前有写锁或者读锁。如果只有读锁,返回false,因为这时如果可以写,那么读线程得到的数据就有可能错误;如果有写锁,但是线程不同,即不符合写锁重入规则,返回false
  2. 如果写锁的数量将会超过最大值65535,抛出异常;否则,写锁重入
  3. 如果没有读锁或写锁的话,如果需要阻塞或者CAS失败,返回false;否则将当前线程置为获得写锁的线程

从上面可以看到调用了writerShouldBlock方法,FairSync的实现是如果等待队列中有等待线程,则返回false,说明公平模式下,只要队列中有线程在等待,那么后来的这个线程也是需要记入队列等待的;NonfairSync中的直接返回的直接是false,说明不需要阻塞。从上面的代码可以得出,当没有锁时,如果使用的非公平模式下的写锁的话,那么返回false,直接通过CAS就可以获得写锁。

根据代码和上面图解总结一下锁获取:

  • 如果当前没有写锁或读锁时,第一个获取锁的线程都会成功,无论该锁是写锁还是读锁。
  • 如果当前已经有了读锁,那么这时获取写锁将失败,获取读锁有可能成功也有可能失败
  • 如果当前已经有了写锁,那么这时获取读锁或写锁,如果线程相同(可重入),那么成功;否则失败

释放锁

获取锁做的是更改AQS的状态值以及将需要等待的线程放入到队列中;释放锁要做的就是更改AQS的状态值以及唤醒队列中的等待线程来继续获取锁。

读锁释放
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第21张图片
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第22张图片

调用tryReleaseShared方法尝试释放锁,如果释放成功,调用doReleaseShared尝试唤醒下一个节点。

Sync中的tryReleaseShared方法实现如下:

 protected final boolean tryReleaseShared(int unused) {
            //得到调用unlock的线程
            Thread current = Thread.currentThread();
            //如果是第一个获得读锁的线程
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            }
            //否则,是HoldCounter中计数-1
            else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            //死循环
            for (;;) {
                int c = getState();
                //释放一把读锁
                int nextc = c - SHARED_UNIT;
                //如果CAS更新状态成功,返回读锁是否等于0;失败的话,则重试
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }

从上面可以看到,释放锁的第一步是更新firstReaderHoldCounter的计数,接下来进入死循环,尝试更新AQS的状态,一旦更新成功,则返回;否则,则重试。

释放读锁对读线程没有影响,但是可能会使等待的写线程解除挂起开始运行。所以,一旦没有锁了,就返回true,否则false;返回true后,那么则需要释放等待队列中的线程,这时读线程和写线程都有可能再获得锁。

写锁释放
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第23张图片
java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第24张图片
调用tryRelease尝试释放锁,一旦释放成功了,那么如果等待队列中有线程再等待,那么调用unparkSuccessor将下一个线程解除挂起。

Sync实现tryRelease方法如下:

java并发编程ReentrantReadWriteLock读写锁详解,图解实现,源码分析,锁降级_第25张图片

写锁释放分为三步:

  1. 如果当前没有线程持有写锁,但是还要释放写锁,抛出异常
  2. 得到解除一把写锁后的状态,如果没有写锁了,那么将AQS的线程置为null
  3. 不管第二步中是否需要将AQS的线程置为null,AQS的状态总是要更新的

从上面可以看到,返回true当且只当没有写锁的情况下,还有写锁则返回false。

根据代码和上面图解总结一下锁释放:

  • 如果当前是写锁被占有了,只有当写锁的数据降为0时才认为释放成功;否则失败。因为只要有写锁,那么除了占有写锁的那个线程,其他线程即不可以获得读锁,也不能获得写锁
  • 如果当前是读锁被占有了,那么只有在写锁的个数为0时才认为释放成功。因为一旦有写锁,别的任何线程都不应该再获得读锁了,除了获得写锁的那个线程。

总结

ReentrantLockReadWriteLock中的state同时表示写锁和读锁的个数。state的高16位表示读锁的个数,低16位表示写锁的个数。

AQS有两种模式:共享模式和独占模式,读写锁的实现中,读锁使用共享模式;写锁使用独占模式

另外一点需要记住的即使,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。

参考:

轻松掌握java读写锁(ReentrantReadWriteLock)的实现原理

深入理解读写锁—ReadWriteLock源码分析

你可能感兴趣的:(java,开发语言,后端)