ReentrantReadWriteLock 源码讲解

ReentrantReadWriteLock 源码讲解

引言

ReentrantLockReentrantReadWriteLock 可以说是双生兄弟,前置是一个独占锁,后者是一个共享锁 ReentrantLock源码讲解

知识点1. 继承AQS的类都需要使用state变量代表某种资源,在ReentrantReadWriteLock中state的高16位代表读锁的个数;低16位代表写锁的状态。

image-20211130204701360.png

1、ReentrantReadWriteLock中的读锁和写锁的实现方式

ReentrantReadWriteLock的源码中又两个重要的属性,如下图,

image-20211127113345458.png

再来看它的构造方法,可以看到ReentrantReadWriteLock也分为公平锁和非公平锁,默认为非公平锁。和ReentrantLock一样都是通过Sync内部类实现。

image-20211127114601260.png 他们两个分别为两个静态内部类对他们的实现

abstract static class Sync extends AbstractQueuedSynchronizer {}

public static class ReadLock implements Lock, java.io.Serializable {
    private final Sync sync; 
    protected ReadLock(ReentrantReadWriteLock lock) {
         sync = lock.sync;
    }
    public void lock() {
        sync.acquireShared(1); //共享
    }
    public void unlock() {
        sync.releaseShared(1); //共享
    }
}
public static class WriteLock implements Lock, java.io.Serializable {
    private final Sync sync;
    protected WriteLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
    public void lock() {
        sync.acquire(1); //独占
    }
    public void unlock() {
        sync.release(1); //独占
    }
}
复制代码

到现在可以看到,ReentrantLockReentrantReadWriteLock两兄弟都是基于AQS来实现的,不同的是:WriteLock和ReentrantLock一样,使用了独占锁。而ReadLock和Semaphore一样,使用了共享锁。

2、ReentrantReadWriteLock的公平锁和非公平锁源码

image-20211127142235597.png 通过上图我们可以看出在ReentrantReadWriteLock中主要是通过他们共同的父类Sync来实现的。再看Sync之前,先介绍一下readerShouldBlockwriterShouldBlock的作用。

writerShouldBlockreaderShouldBlock方法都表示当有别的线程也在尝试获取锁时,是否应该阻塞。

其中公平锁的hasQueuedPredecessors是我们老朋友,在ReentrantLock中的公平锁中我们见过他的身影,它的作用是用来判断当前的线程是不是队列当中的第一个,是第一个则返回ture,不是第一个则返回false。

非公平模式下,writerShouldBlock直接返回false,说明不需要阻塞;而readShouldBlock调用了apparentFirstQueuedIsExcluisve()方法。该方法在当前线程是写锁占用的线程时,返回true;否则返回false。也就说明,如果当前有一个写线程正在写,那么该读线程应该阻塞。

3、读锁的获取

先查看读锁的获取流程

4236553-c747934c55844272.png 锁的获取看下图。当tryAcquireShared()方法小于0时,那么会执行doAcquireShared方法将该线程加入到等待队列中。

image-20211130203251441.png

image-20211130203329060.png Sync实现了tryAcquireShared方法,如下:

protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
     // 如果当前有写线程并且本线程不是写线程,不符合重入,失败 
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
     // 得到读锁的个数
            int r = sharedCount(c);
     // 1.判断是否应该阻塞 
     //   公平锁和非公平锁判断阻塞的方式不一样、具体查看公平锁和非公平锁源码
     // 2.判断读锁的个数是不是小于65535
     // 3.CAS更新读锁的值
            if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
     		// 如果当前读锁为0做初始化的操作
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
     		// 因为是可重入锁,所以当冲入的时候读的计数器+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 1;
            }
     // 否则,循环尝试 什么时候会循环尝试换取
     // 1.公平锁的且有其他线程在排队时
     // 2.读锁的个数>=65535时,需等待其他释放
     // 3.CAS失败时
            return fullTryAcquireShared(current);
        }
复制代码
  • cachedHoldCounter是最后一个获取到读锁的线程计数器,每当有新的线程获取到读锁,这个变量都会更新。这个变量的目的是:当最后一个获取读锁的线程重复获取读锁,或者释放读锁,就会直接使用这个变量,速度更快,相当于缓存。并且每一个线程没都有自己的计数器。

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()) {
                    if (firstReader == current) {
             // 说明有别的读线程占有了锁
                    } 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;
                }
            }
        }
复制代码

可以看到fullTryAcquireShared与tryAcquireShared有很多类似的地方。

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

4、写锁的获取

写锁的lock方法如下:image-20211201145937760.png image-20211201150007606.png 从上面可以看到,写锁使用的是AQS的独占模式。首先尝试获取锁,如果获取失败,那么将会把该线程加入到等待队列中。

protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
    // 得到写锁的个数 
            int w = exclusiveCount(c);
    // 如果当前有写锁或者读锁
            if (c != 0) {
            // 写锁为0或者但当前线程等于不是独占的线程获取锁失败
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
            // 写锁超过最大值报错
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
           // 设置状态获取成功
                setState(c + acquires);
                return true;
            }
    // 如果当前没有写锁或者读锁,写线程应该阻塞或者CAS失败,返回false
            if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
                return false;
    // 否则将当前线程置为获得写锁的线程,返回true
            setExclusiveOwnerThread(current);
            return true;
        }
复制代码

获取锁失败加入等待队列,请查看我上一篇文章ReentrantLock

小结

如果当前没有写锁或读锁时,第一个获取锁的线程都会成功,无论该锁是写锁还是读锁。

如果当前已经有了读锁,那么这时获取写锁将失败,获取读锁有可能成功也有可能失败。

如果当前已经有了写锁,那么这时获取读锁或写锁,如果线程相同(可重入),那么成功;否则失败。

5、读锁的释放

话不多说,上图

image-20211201161301869.png doReleaseShared() 是释放成功之后去唤醒下一个节点

image-20211201161323819.png tryReleaseShared()源码的具体实现如下

 protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
      // 如果是第一个获得读锁的线程
            if (firstReader == current) {
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            }
     // 当前线程的计数器-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();
                //释放一把读锁 因为高16位代表读锁,所以-65536
                int nextc = c - SHARED_UNIT;
                // 更新成功返回,否者尝试
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
复制代码

6、写锁的释放

image-20211201170520098.png

image-20211201170534267.png

tryReleasevi释放写锁,释放完成之后,如果队列中还有其他的线程那么需要去唤醒堵塞的线程。

 protected final boolean tryRelease(int releases) {
     // 如果没有线程持有写锁,但是仍要释放,抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
     // 读锁是否全部释放
            boolean free = exclusiveCount(nextc) == 0;
     // 如果没有写锁了,那么将AQS的线程置为null
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
}
复制代码

小结

从上面得源码可以看出:

1、如果当前是写锁被占有了,只有当写锁的数据降为0时才认为释放成功;否则失败。因为只要有写锁,那么除了占有写锁的那个线程,其他线程即不可以获得读锁,也不能获得写锁。

2、如果当前是读锁被占有了,那么只有在写锁的个数为0时才认为释放成功。因为一旦有写锁,别的任何线程都不应该再获得读锁了,除了获得写锁的那个线程。

本人能力有限,写的不太好请各位看官不要纠结

本人能力有限,写的不太好请各位看官不要纠结

本人能力有限,写的不太好请各位看官不要纠结

参考连接

并发编程之——读锁源码分析(解释关于锁降级的争议)

并发进阶(十二)读写锁

ReadWriteLock源码分析

猜你喜欢

转载自juejin.im/post/7037033112227807269