【Java并发编程】读写锁ReentrantReadWriteLock实现原理及源码分析

        前两篇我们分析了AQS的独占锁和共享锁的实现原理,本篇文章将继续分析AQS的实现者ReentrantReadWriteLock的实现原理!

        读写锁维护了一对相关的锁,即读锁和写锁,读锁是共享锁,允许多个线程同时访问资源,而写锁是独占锁,任一时刻只允许一个线程占有独占锁。读写锁适用于读多写少的情况。首先,思考以下几个问题!

1.我们知道AQS维护着一个同步状态state,那么读写锁是如何协调读写线程的呢?

2.读写锁如何实现公平性?

实现原理


1.读写状态的控制

AQS 的状态state是32位(int 类型)的,辦成两份,读锁用高16位,表示持有读锁的线程数(sharedCount),写锁用低16位,表示写锁的重入次数 (exclusiveCount)。状态值为 0 表示锁空闲,sharedCount不为 0 表示分配了读锁,exclusiveCount 不为 0 表示分配了写锁,sharedCount和exclusiveCount 一般不会同时不为 0,只有当线程占用了写锁,该线程可以重入获取读锁,反之不成立。

读写锁

 2.公平模式和非公平模式

公平模式:判断同步队列是否有等待线程。一旦前面有等待线程,那么为了遵循公平,当前线程也就应该加入同步队列。 

非公平模式:

如果线程想获取写锁,那么写线程不应该阻塞。

如果线程想获取读锁,通常不需要阻塞,除了这样一种情况:当全局处于读锁状态,且等待队列中第一个等待线程想获取写锁,那么当前线程能够获取到读锁的条件为:当前线程获取了写锁,还未释放;当前线程获取了读锁,这一次只是重入读锁而已;其它情况当前线程入队尾。之所以这样处理一方面是为了效率,一方面是为了避免想获取写锁的线程饥饿,老是得不到执行的机会。

例如:线程C请求一个写锁,由于当前其他两个线程拥有读锁,写锁获取失败,线程C入队列(根据规则i),如下所示

读写锁

AQS初始化会创建一个空的头节点,C入队列,然后会休眠,等待其他线程释放锁唤醒。

此时线程D也来了,线程D想获取一个读锁,上面规则,队列中第一个等待线程C请求的是写锁,为避免写锁迟迟获取不到,并且线程D不是重入获取读锁,所以线程D也入队,如下图所示:

之所以这样处理一方面是为了效率,一方面是为了避免想获取写锁的线程饥饿,老是得不到执行的机会。

 继承关系


首先通过一张继承关系图从总体上了解读写锁的实现原理。

读写锁继承关系图

首先Sync类继承了AQS,Sync对AQS中的同步状态state一分为二,高十六位表示读锁计数,低十六位表示写锁计数,并重写了四个最重要的方法,tryAcquire和tryRealse方法定义了获取独占锁的相关规则,tryAcquireShared和tryRealseShared方法定义了获取共享锁的相关规则。

ReentrantReadWriteLock通过引用Sync,并将该引用传递给引用ReadLock和WriteLock,这样ReadLock和WriteLock使用的是同一个Sync,通过控制同一个同步状态state来控制读写线程。

源码分析


首先看一下对state同步状态一分为二,读写锁计数。

abstract static class Sync extends AbstractQueuedSynchronizer {
  
       static final int SHARED_SHIFT   = 16;
       // 由于读锁用高位部分,所以读锁个数加1,其实是状态值加 2^16
       static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
       // 写锁的可重入的最大次数、读锁允许的最大数量
       static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
       // 写锁的掩码,用于状态的低16位有效值
       static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
       // 读锁计数,当前持有读锁的线程数
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    // 写锁的计数,也就是它的重入次数
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}

我们知道读写锁支持多个读线程同时拥有读锁,那么如何控制每个线程重入读锁的次数呢?

abstract static class Sync extends AbstractQueuedSynchronizer {
    /**
     * 每个线程特定的 read 持有计数。存放在ThreadLocal,不需要是线程安全的。
     */
    static final class HoldCounter {
        int count = 0;
        // 使用id而不是引用是为了避免保留垃圾。注意这是个常量,一旦创建不能更改。
        final long tid = Thread.currentThread().getId();
    }
    /**
     * 采用继承是为了重写 initialValue 方法,这样就不用进行这样的处理:
     * 如果ThreadLocal没有当前线程的计数,则new一个,再放进ThreadLocal里。
     * 可以直接调用 get,ThreadLocal调用get方法时,如果为空会调用initialValue方法。
     * */
    static final class ThreadLocalHoldCounter
        extends ThreadLocal<HoldCounter> {
        public HoldCounter initialValue() {
            return new HoldCounter();
        }
    }
    /**
     * 保存当前线程重入读锁的次数的容器。在读锁重入次数为 0 时移除。
     */
    private transient ThreadLocalHoldCounter readHolds;
    /**
     * 最近一个成功获取读锁的线程的计数。
     * 通常情况下,下一个释放线程是最后一个获取线程。这不是 volatile 的,
     * 仅用作借鉴缓存,可以在一定程度上避免访问readHolds中的HoldCounter。
     * (因为判断是否是当前线程是通过线程id来比较的)。
     */
    private transient HoldCounter cachedHoldCounter;
    /**
     * firstReader是这样一个特殊线程:它是最后一个把 共享计数 从 0 改为 1 的
     * (在锁空闲的时候),而且从那之后还没有释放读锁的(释放了读锁,firstReader会重置为
     * null)。如果不存在则为null。
     * firstReaderHoldCount 是 firstReader 的重入计数。
     *
     * firstReader 不能导致保留垃圾,因此在 tryReleaseShared 里设置为null。
     * 除非线程异常终止,没有释放读锁。
     *
     * 作用是在跟踪无竞争的读锁计数时非常便宜。
     *
     * firstReader及其计数firstReaderHoldCount是不会放入 readHolds 的。
     */
    private transient Thread firstReader = null;
    private transient int firstReaderHoldCount;
    Sync() {
        readHolds = new ThreadLocalHoldCounter();
// 确保 readHolds 的内存可见性,利用 volatile 内存语义,即禁止重排序。
        setState(getState()); 
    }
}

这里有一个疑问,既然readHolds可以保存所有线程的重入计数,咋还使用了firstReader和firstReaderHoldCount单独保存第一个读线程重入计数。cachedHoldCounter保存最后一个读线程的重入计数。

源码多次使用firstReader和cachedHoldCounter来进行重入计数判断,如果不是才使用readHolds先读取在设置重入计数。

比如只有一个读线程获取读锁,那么也就没有必要设置ThreadLocal变量readHolds。这里顺便说一下ThreadLocal的缺点:

1.  容易造成内存泄漏。thread local 是实际是两级以上的hashtable,一旦你还用线程池的话,用的不好可能永远把内存占着。造成java VM上的内存泄漏。
2. 代码的耦合度高,且测试不易。

具体原因的可以上网百度。

写锁的获取


protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
//1.获取同步状态
            int c = getState();
//2.获取写锁计数
            int w = exclusiveCount(c);
//3.如果同步状态不等于0
            if (c != 0) {
                // 4.如果同步状态不等于0,且写锁计数=0,说明读锁计数不等于0
                //或者同步状态不等于0,写锁不等于0,并且非重入,那么返回false
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //5.如果写锁计数即将超过最大数,抛出异常(写锁数量超出最大值)
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                //6.否则就更新state,重入写锁成功
                setState(c + acquires);
                return true;
            }
//7.如果同步状态为0,队列政策允许,就CAS设置state
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
//8.设置state成功,就设置独占锁拥有着owner,获取写锁成功。
            setExclusiveOwnerThread(current);
            return true;
        }

由上面分析可以得出写锁获取的条件:

1.如果存在读线程正在占用读锁,则写锁获取失败。

2.如果没有读线程,但存在写线程正在占用写锁,除非是重入写锁,否则获取失败。

3.如果没有读写线程占用读写锁,如果队列政策允许就获取写锁。

写锁的释放


protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
//由于写锁是独占锁,所以不用考虑并发安全问题。
            int nextc = getState() - releases;
//1.释放写锁后的独占计数是否等于0。
            boolean free = exclusiveCount(nextc) == 0;
//2.写锁计数为0,就设置独占线程引用为null
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

相对于写锁的获取,写锁的释放较为简单,因为写锁是独占锁,不用考虑并发安全问题。

读锁的获取


protected final int tryAcquireShared(int unused) {
           
            Thread current = Thread.currentThread();
            int c = getState();
//1.如果写锁计数不等于0,且非重入,返回-1,获取读锁失败。
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
//2.写锁计数等于0,可以获取读锁
//3.读锁计数
            int r = sharedCount(c);
//4。此处首先判断队列政策是否允许获取读锁,如果允许在判断读锁计数是否小于最大值,如果小于
//继续通过CAS设置同步状态,设置成功,即表示获取读锁成功。
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {
//5.如果读锁计数为0,说明当前线程是新的第一个获取读锁的线程
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
//6.如果当前线程是第一个获取读锁的线程,那么firstReaderHoldCount增1
                    firstReaderHoldCount++;
                } else {
//7.否则,先通过缓存线程计数,判断是否是当前线程,避免了访问readHolds
                    HoldCounter rh = cachedHoldCounter;
//8.如果缓存线程计数为null或者缓存线程计数不是当前线程,那么从readHolds中取。
                    if (rh == null || rh.tid != current.getId())
                        cachedHoldCounter = rh = readHolds.get();
//9.如果缓存线程计数是当前线程,并且count=0,那么需要重新设置readHolds中的缓存线程计数。
//因为当count=0时,缓存线程计数会从readHolds中删除,故需要重新设置
                    else if (rh.count == 0)
                        readHolds.set(rh);
//10.count自增。
                    rh.count++;
                }
//11.获取读锁成功,返回1
                return 1;
            }
//12.否则,获取失败就调用fullTryAcquireShared循环获取读锁。
            return fullTryAcquireShared(current);
        }

注意:

第5点,如果读锁计数为0,说明当前线程是新的第一个获取读锁的线程。而firstReader和firstReaderHoldCount并没有使用volatile修饰,也没有保证原子性,那么如何保证线程安全的呢?

因为firstReader代表新的第一个将读锁计数从0变为1的线程,有且只有一个,所以不存在并发。有人就会说,可能存在多个线程尝试将读锁计数从0变1,这就是设计者精妙之处,在进入第五处代码之前,是先通过CAS设置了同步状态,故有且只有一个线程能够成功。

注意:firstReader及其计数firstReaderHoldCount是不会放入 readHolds 的。firstReader是这样一个特殊线程:它是最后一个把 共享计数 从 0 改为 1 的而且从那之后还没有释放读锁的。如果不存在则为null。

fullTryAcquireShared是获取读锁的最全版本,用来处理在tryAcquireShared中CAS设置失败,或者重入读未处理。

final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
//1.如果写锁计数不等于0,且非重入,返回-1,获取读锁失败。
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                } else if (readerShouldBlock()) {
//2.如果队列政策认为当前线程应该阻塞
                    
                    if (firstReader == current) {
// 3.如果当前线程是firstReader,那么firstReaderHoldCount > 0,即当前线程还没有释放读锁,
//这次是重入读锁;
                    } else {
                        if (rh == null) {
//4.如果不是firstReader,先看看是否是最后线程cachedHoldCounter
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != current.getId()) {
                                rh = readHolds.get();
//5.如果当前线程也不是最后线程cachedHoldCounter,那么就通过readHolds获取HoldCounter
//6.如果当前线程读锁计数为0,那么就从readHolds移除本线程的HoldCounter
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
//7.如果当前线程读锁计数为0,那么此次线程非重入读锁,返回-1,获取读锁失败。
                        if (rh.count == 0)
                            return -1;
                    }
                }
//8.代码到这,表示可以获取读锁
//9.如果读锁计数已经达到最大值。
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
//10.如果CAS设置同步状态成功,下面步骤就和tryAquiredShared一样了,参考tryAquiredShared
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != current.getId())
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

注意:第2处,即使readerShouldBlock()返回true,即队列政策认为当前线程应该阻塞,也要判断是否是读锁重入。此处参考最上面的实现原理2的分析图。

读锁的释放


protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                //1.如果当前线程是firstReader,那么firstReaderHoldCount > 0;
                //2.如果firstReaderHoldCount=1,那么这次释放读锁,就需要设置firstReader=null
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                //3.否则firstReader读锁计数自减
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
//4.否则,就先判断当前是否是cachedHoldCounter
                if (rh == null || rh.tid != current.getId())
//5.如果不是,就从readHolds中读取
                    rh = readHolds.get();
                int count = rh.count;
//6.如果count=1,那么就需要从readHolds移除当前线程计数
                if (count <= 1) {
                    readHolds.remove();
//7.如果count=0,抛出异常
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
//8.读锁计数自减
                --rh.count;
            }
//9.到此,就处理好了当前线程的读锁计数。下面就循环CAS更新同步状态,直到成功。
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                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;
            }
        }

至此,读写锁的关键点已经分析完成了,接下来,我们还会分析JUC中其他的并发组件。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值