专栏首页晏霖Java并发之-读写锁ReentrantReadWriteLock

Java并发之-读写锁ReentrantReadWriteLock

前言

之前提到的ReentrantLock是排他锁,这种锁同一时刻只允许一个线程访问,而读写锁同一时刻可以多个线程访问,但在写线程访问时,所有读线程和其他写线程都要被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读写锁,使得并发性相比一般的排他锁有很大提升。

参考文献

《Java并发编程的艺术》

正文

读写锁只需要在读操作时获取读锁,写操作获取写锁即可。当写操作被获取时,后续读写锁都会被阻塞,写操作释放以后,所有操作继续执行。

一般情况下,读写锁的性能比排他锁要好,因为大多数场景读是多于写的,所以在读多余写时,读写锁能够提供比排他锁更好的性能和吞吐量。java中读写锁实现是 ReentrantReadWriteLock。

特性

说明

公平性

支持非公平性(默认)和公平性获取方式,吞吐量还是非公平性优于公平性。

重入性

支持重入。读线程获取读后,能够再次获取读锁。而写线程获取写锁后能再次获取写锁,同时可以获取读锁。

锁降级

遵循获取写锁、获取读锁再释放写锁的次序,写锁降级为读锁。

读写锁的接口

ReadWriteLock定义了获取读锁和写锁的两个方法分别是,readLock() 方法和 writeLock() 方法,而其实现

是 ReentrantReadWriteLock ,其内部还有一些工作状态的方法,如下。

方法

说明

int getReadLockCount()

返回当前读锁被获取读次数,同一个线程获取了n次,该方法返回n。

int getReadHoldCount()

返回当前线程获取读锁读次数

boolean isWriteLocked()

判断写锁是否被获取。

int getWriteHoldCount()

返回当前线程获取写锁的次数。

可以观察规律凡是方法名带有 hold 的方法都是针对当前线程而言的,反之针对当前锁而言。

读写锁一般使用

static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    public static final Object get(String key) {
        r.lock();
        try {
            return null;

        } finally {
            r.unlock();
        }
    }

    public static final Object clean() {
        w.lock();
        try {
            return null;

        } finally {
            w.unlock();
        }
    }

读写锁依赖自定义的同步器来实现的其功能,读写状态就是同步器的同步状态。

与ReentrantLock不同在于,读写锁自定义同步器需要在同步状态上维护多个读线程和一个写线程的状态,所以状态的设计成为了关键。

我们要在一个整型变量维护多种状态就要按位切割使用这个整型的变量,读写锁将整形变量切分成两部分,高16位表示读,低16位表示写,如图。

如图所示,当前同步状态表示一个线程已经获取了锁,且重入了两次,同时也连续获取了两次读锁。读写锁是通过位运算来快速定位读和写各自状态的。假设当前同步状态的值为S,写状态就为 S & 0x0000FFFF(将高16为全部抹去),读状态为 S >>> 16(无符号位补0右移16位)。当写状态增加 1 状态变为 S+1,当读状态增加1时,状态编码就是 S+(1 << 16)也就是 S+0x00010000。

结论:S不等于0时,当写状态(S&0x0000FFFF) 等于0时,则读状态 (S>>> 16) 大于0 ,即读锁已经获取。

写锁的获取与释放

写锁支持重入的排他锁。当一个线程获取了写锁,则增加状态只。如果当前线程在获取写锁时,读写已经被获取,或者该线程不是获取写锁的线程,则当前线程进入阻塞。

protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // 存在读锁或者当前获取线程不是已经获取写锁的线程
                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;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

代码解释:可能有人早已疑问我代码里注释的那句话是什么意思了,为什么要判断以下读锁是否存在,存在读锁的话,写锁就不能获取。原因在于:读写锁要确保写锁的操作对读锁的可见性,如果允许读锁已经被获取的情况下还要获取写锁,那么正在运行的其他读线程就无法感知当前写线程的操作。所以说获取写锁前一定要看是否还有读锁已经被获取。而写锁一旦被获取,其他读写线程都要被阻塞了。说白了,我们要保证在写之前不能有线程还在读,这样数据不准确。

读锁读获取与释放

读相对于写限制就少了写,读锁支持重入和共享,也就是同时可以被多个线程访问,在没有其他写线程访问时,所有读操作总是不会阻塞的,就是都能获取到。但是如果当前线程要获取读锁时,发现写锁已经被获取,那么读锁要进入等待状态。

如果其他线程已经获取了写锁,则当前线程回去读锁失败,进入等待。

如果当前线程获取了写锁或者写锁为被获取,则当前线程增加读状态,读锁获取成功。

锁降级

锁降级指的是写锁降级为读锁,是指把持主写锁,再次获取到读锁,随后释放写锁到过程。

锁降级是有必要到,是保证数据到可见性,如果当前线程不获取读锁而是直接释放写锁,假设此时另一个线程T获取了写锁并修改了数据,那么当前线程无法感知线程T修改了数据。现在有遵循锁降级步骤,当前线程获取读锁后,就不应该有写锁再被获取,要等读锁释放后,写锁才可以被获取。另外ReentrantReadWriteLock不支持锁升级。

感兴趣的小伙伴,可以加为微信,进入java聊天群

本文分享自微信公众号 - 晏霖(yanlin199507)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-03-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【Mysql中间件】Mycat安装部署+读写分离

    说明: mysql-master:172.16.200.43 Mycat:172.16.200.43 mysql-slave1:172.16.200.45 my...

    用户5522200
  • kakfa学习总结

    KAFKA是Apache基金会的一个开源项目,是一个分布式的发布-订阅的消息系统;

    用户5522200
  • Java 中处理异常的 9 个实践

    在本文中,介绍了 9 个处理异常的最佳方法与实践,以举例与代码展示结合的方式,让开发者更好的理解这 9 种方式,并指导读者在不同情况下选择不同的异常处理方式。

    周三不加班
  • 处理java访问mysql连接数太多的错误

    在生产环境处理故障的过程出现了java服务连接mysql,由于连接数太多被拒绝连接的故障,那么下面来看看怎么优化一下吧。

    Devops海洋的渔夫
  • 喝杯奶茶来了解装饰模式?

    使用到装饰模式的情况,关键之处就是在于装饰两字,翻译过来就是需要为一个类或者对象添加额外的功能,而且这个功能可能是出于特定的情况之下才会需要的。

    程序员小强
  • Java线程安全面试题,你真的了解吗?

    多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。

    李红
  • Python 迭代器 - Iterable对象

    迭代是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。

    Devops海洋的渔夫
  • Java 学习笔记(7)——接口与多态

    上一篇说了Java面向对象中的继承关系,在继承中说到:调用对象中的成员变量时,根据引用类型来决定调用谁,而调用成员方法时由于多态的存在,具体调用谁的方法需要根据...

    Masimaro
  • 高效率使用 Github

    此处默认你有 Github 账号、安装了 Git 并且熟悉基本的 Git 操作,只是需要寻求部署 Github Pages 方面的知识。GitHub Pages...

    周三不加班
  • Java并发-22.阻塞队列

    无界阻塞队列永远不满,put和offer方法永远不阻塞,offer永远返回true

    悠扬前奏

扫码关注云+社区

领取腾讯云代金券