【原创】Java并发编程系列18 | 读写锁(下)

2021-03-15 04:30

阅读:526

标签:unlock   stat   cas   基本   理解   执行   shel   res   nbsp   

【原创】Java并发编程系列18 | 读写锁(下)

收录于话题
#进阶架构师 | 并发编程专题
12个

技术图片
技术图片
本文为何适原创并发编程系列第 18 篇,文末有本系列文章汇总。
通过以下几部分来分析Java提供的读写锁ReentrantReadWriteLock:
为什么需要读写锁
读写锁的使用Demo
ReentrantReadWriteLock类结构
记录读写锁状态
源码分析读锁的获取与释放
源码分析写锁的获取与释放
锁降级
读写锁应用
本文涉及到上下文联系较多,经常需要上下滑动查看,篇幅太多很不方便,而且文章太长阅读体验也不好,所以分成读写锁(上)和读写锁(下)两篇。上篇为【原创】Java并发编程系列17 | 读写锁八讲(上),没看过的可以先看看。本文是下篇,从“源码分析写锁的获取与释放”开始。

7. 写锁获取


rwl.writeLock().lock()的调用

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

public final void acquire(int arg) {
    if (!tryAcquire(arg) && // 写锁实现了获取锁的方法,下文详细讲解
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 获取锁失败进入同步队列,等待被唤醒,AQS一文中重点讲过
        selfInterrupt();
}

先分析一下可以获取写锁的条件:
当前锁的状态
1)没有线程占用锁(读写锁都没被占用) 2)线程占用写锁时,线程再次来获取写锁,也就是重入
AQS队列中的情况,如果是公平锁,同步队列中有线程等锁时,当前线程是不可以先获取锁的,必须到队列中排队。
写锁的标志位只有16位,最多重入2^16-1次。

/**
 * ReentrantReadWriteLock.Sync.tryAcquire(int)
 */
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);// 写锁标志位

    // 进到这个if里,c!=0表示有线程占用锁
    // 当有线程占用锁时,只有一种情况是可以获取写锁的,那就是写锁重入
    if (c != 0) {
        /*
         * 两种情况返回false
         * 1.(c != 0 & w == 0)
         * c!=0表示标志位!=0,w==0表示写锁标志位==0,总的标志位不为0而写锁标志位(低16位)为0,只能是读锁标志位(高16位)不为0
         * 也就是有线程占用读锁,此时不能获取写锁,返回false
         *
         * 2.(c != 0 & w != 0 & current != getExclusiveOwnerThread())
         * c != 0 & w != 0 表示写锁标志位不为0,有线程占用写锁
         * current != getExclusiveOwnerThread() 占用写锁的线程不是当前线程
         * 不能获取写锁,返回false
         */
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 重入次数不能超过2^16-1
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");

        /*
         * 修改标志位
         * 这里修改标志位为什么没有用CAS原子操作呢?
         * 因为到这里肯定是写锁重入了,写锁是独占锁,不会有其他线程来捣乱。
         */
        setState(c + acquires);
        return true;
    }

    /*
     * 到这里表示锁是没有被线程占用的,因为锁被线程占用的情况在上个if里处理并返回了
     * 所以这里直接检查AQS队列情况,没问题的话CAS修改标志位获取锁
     */
    if (writerShouldBlock() || // 检查AQS队列中的情况,看是当前线程是否可以获取写锁
        !compareAndSetState(c, c + acquires)) // 修改写锁标志位
        return false;
    setExclusiveOwnerThread(current);// 获取写锁成功,将AQS.exclusiveOwnerThread置为当前线程
    return true;

}
简单看下writerShouldBlock()
writerShouldBlock():检查AQS队列中的情况,看是当前线程是否可以获取写锁,返回false表示可以获取写锁。
对于公平锁来说,如果队列中还有线程在等锁,就不允许新来的线程获得锁,必须进入队列排队。
hasQueuedPredecessors()方法在重入锁的文章中分析过,判断同步队列中是否还有等锁的线程,如果有其他线程等锁,返回true当前线程不能获取读锁。


// 公平锁
final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}

对于非公平锁来说,不需要关心队列中的情况,有机会直接尝试抢锁就好了,所以直接返回false。


// 非公平锁
final boolean writerShouldBlock() {
    return false;
}

8. 写锁释放


写锁释放比较简单,跟之前的重入锁释放基本类似,看下源码:

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

/**
 * 释放写锁,如果释放之后没有线程占用写锁,唤醒队列中的线程来获取锁
 */
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);// 唤醒head的后继节点去获取锁
        return true;
    }
    return false;
}

/**
 * 释放写锁,修改写锁标志位和exclusiveOwnerThread
 * 如果这个写锁释放之后,没有线程占用写锁了,返回true
 */
protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

9. 锁降级


读写锁支持锁降级。锁降级就是写锁是可以降级为读锁的,但是需要遵循获取写锁、获取读锁、释放写锁的次序。
为什么要支持锁降级?

支持降级锁的情况:线程A持有写锁时,线程A要读取共享数据,线程A直接获取读锁读取数据就好了。
如果不支持锁降级会怎么样?
线程A持有写锁时,线程A要读取共享数据,但是线程A不能获取读锁,只能等待释放写锁。
当线程A释放写锁之后,线程A获取读锁要和其他线程抢锁,如果另一个线程B抢到了写锁,对数据进行了修改,那么线程B释放写锁之后,线程A才能获取读锁。线程B获取到读锁之后读取的数据就不是线程A修改的数据了,也就是脏数据。
源码中哪里支持锁降级?

tryAcquireShared()方法中,当前线程占用写锁时是可以获取读锁的,如下:

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    /*
     * 根据锁的状态判断可以获取读锁的情况:
     * 1. 读锁写锁都没有被占用
     * 2. 只有读锁被占用
     * 3. 写锁被自己线程占用
     * 总结一下,只有在其它线程持有写锁时,不能获取读锁,其它情况都可以去获取。
     */
    if (exclusiveCount(c) != 0 && // 写锁被占用
        getExclusiveOwnerThread() != current) // 持有写锁的不是当前线程
        return -1;
    ...

不支持锁升级

持有写锁的线程,去获取读锁的过程称为锁降级;持有读锁的线程,在没释放的情况下不能去获取写锁的过程称为锁升级。
读写锁是不支持锁升级的。获取写锁的tryAcquire()方法:

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    /*
     * (c != 0 & w == 0)时返回false,不能获取写锁
     * c != 0 表示state不是0
     * w == 0 表示写锁标志位state的低16位为0
     * 所以state的高16位不为0,也就是有线程占有读锁
     * 也就是说只要有线程占有读锁返回false,不能获取写锁,当然线程自己持有读锁时也就不能获取写锁了
     */
    if (c != 0) {
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
            ...

8. 应用


读写锁多用于解决读多写少的问题,最典型的就是缓存问题。如下是官方给出的应用示例:

class CachedData {
    Object data;
    volatile boolean cacheValid;
    // 读写锁实例
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        // 获取读锁
        rwl.readLock().lock();
        if (!cacheValid) { // 如果缓存过期了,或者为 null
            // 释放掉读锁,然后获取写锁 (后面会看到,没释放掉读锁就获取写锁,会发生死锁情况)
            rwl.readLock().unlock();
            rwl.writeLock().lock();

            try {
                if (!cacheValid) { // 重新判断,因为在等待写锁的过程中,可能前面有其他写线程执行过了
                    data = ...
                    cacheValid = true;
                }
                // 获取读锁 (持有写锁的情况下,是允许获取读锁的,称为 “锁降级”,反之不行。)
                rwl.readLock().lock();
            } finally {
                // 释放写锁,此时还剩一个读锁
                rwl.writeLock().unlock(); // Unlock write, still hold read
            }
        }

        try {
            use(data);
        } finally {
            // 释放读锁
            rwl.readLock().unlock();
        }
    }
}

总结


可以获取写锁的情况只有两种:
读锁和写锁都没有线程占用
当前线程占用写锁,也就写锁重入
读写锁支持锁降级,不支持锁升级。锁降级就是写锁是可以降级为读锁的,但是需要遵循获取写锁、获取读锁、释放写锁的次序。
读写锁多用于解决读多写少的问题,最典型的就是缓存问题。
参考资料
《Java 并发编程之美》
《Java 并发编程实战》
《Java 并发编程的艺术》

并发系列文章汇总


【原创】01|开篇获奖感言
【原创】02|并发编程三大核心问题
【原创】03|重排序-可见性和有序性问题根源
【原创】04|Java 内存模型详解
【原创】05|深入理解 volatile
【原创】06|你不知道的 final
【原创】07|synchronized 原理
【原创】08|synchronized 锁优化
【原创】09|基础干货
【原创】10|线程状态
【原创】11|线程调度
【原创】13|LockSupport
【原创】14|AQS 源码分析
【原创】15|重入锁 ReentrantLock
【原创】16|公平锁与非公平锁
【原创】Java并发编程系列17 | 读写锁八讲(上)

之前,给大家发过三份Java面试宝典,这次新增了一份,目前总共是四份面试宝典,相信在跳槽前一个月按照面试宝典准备准备,基本没大问题。
《java面试宝典5.0》(初中级)
《350道Java面试题:整理自100+公司》(中高级)
《资深java面试宝典-视频版》(资深)
《Java[BAT]面试必备》(资深)
分别适用于初中级,中高级,资深级工程师的面试复习。
内容包含java基础、javaweb、mysql性能优化、JVM、锁、百万并发、消息队列,高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper、数据结构、限流熔断降级等等。
技术图片
获取方式:点“在看”,V信关注上述单号并回复 【面试】即可领取,更多精彩陆续奉上。

看到这里,证明有所收获
必须点个在看支持呀,喵

【原创】Java并发编程系列18 | 读写锁(下)

标签:unlock   stat   cas   基本   理解   执行   shel   res   nbsp   

原文地址:https://blog.51cto.com/15009303/2552735


评论


亲,登录后才可以留言!