【漫画】互斥锁ReentrantLock很差用?试试读写锁ReadWriteLock

ReentrantLock完美实现了互斥,完美解决了并发问题。可是却意外发现它对于读多写少的场景效率实在不行。此时ReentrantReadWriteLock来救场了!一种适用于读多写少场景的锁,能够大幅度提高并发效率,你必须会哦!java

序幕

_1

为什么引入读写锁?

ReentrantReadWriteLock,顾名思义,是可重用的读写锁。数据库

在读多写少的场合,读写锁对系统性能是颇有好处的。由于若是系统在读写数据时均只使用独占锁,那么读操做和写操做间、读操做和读操做间、写操做和写操做间均不能作到真正的并发,而且须要相互等待。而读操做自己不会影响数据的完整性和一致性。编程

所以,理论上讲,在大部分状况下,应该能够容许多线程同时读,读写锁正是实现了这种功能。缓存

划重点:读写锁适用于读多写少的状况。能够优化性能,提高易用性。微信

读写锁 ReadWriteLock

读写锁,并非 Java 语言特有的,而是一个广为使用的通用技术,全部的读写锁都遵照如下三条基本原则:多线程

  • 容许多个线程同时读共享变量;
  • 只容许一个线程写共享变量;
  • 若是一个写线程正在执行写操做,此时禁止读线程读共享变量。

读写锁与互斥锁的一个重要区别就是读写锁容许多个线程同时读共享变量,而互斥锁是不容许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但读写锁的写操做是互斥的、独占的,当一个线程在写共享变量的时候,是不容许其余线程执行写操做和读操做。只要没有写操做,读取锁能够由多个读线程同时保持。读写锁访问约束以下表所示:并发

读写锁
非阻塞 阻塞
阻塞 阻塞

读写锁维护了一对相关的锁,一个用于只读操做,一个用于写入操做。性能

private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    //读锁
    private final Lock r = rwl.readLock();
    //写锁
    private final Lock w = rwl.writeLock();

为了对比读写锁和独占锁的区别,咱们能够写一个测试代码,分别传入ReentrantLock 和 ReadLock,对比一下总耗时。测试

private static final ReentrantLock lock = new ReentrantLock();
    private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private static final Lock r = rwl.readLock();

    public static String read(Lock lock, String key) throws InterruptedException {
        r.lock();
        try {
            // 模拟读耗时多的场景 更能看出区别
            Thread.sleep(1000 * 10);
            return m.get(key);
        } finally {
            r.unlock();
        }
    }

快速实现一个缓存

回想一下工做中常常用到的缓存,例如缓存元数据,不就是一种典型的读多写少应用场景吗?缓存之因此能提高性能,一个重要的条件就是缓存的数据必定是读多写少的,例如元数据和基础数据基本上不会发生变化(写少),可是使用它们的地方却不少(读多)。大数据

咱们是否是能够用ReentrantReadWriteLock来手写一个缓存呢?先画一张图模拟简单的缓存流程吧:
未命名文件.png
_2

_3

String get(String key) throws InterruptedException {
        String v = null;
        r.lock();
        log.info("{}获取读锁 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
        try {
            v = m.get(key);
        } finally {
            r.unlock();
            log.info("{}释放读锁 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
        }
        if (v != null) {
            log.info("{}缓存存在,返回结果 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
            return v;
        }
        w.lock();
        log.info("{}缓存中不存在,查询数据库,获取写锁 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
        try {
            log.info("{}二次验证 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
            v = m.get(key);
            if (v == null) {
                log.info("{}查询数据库完成 time={} ",Thread.currentThread().getName(),System.currentTimeMillis());
                v = "value";
                log.info("-------------验证写锁占有的时候 其余线程没法执行写操做和读操做----------------");
                Thread.sleep(1000*5);
                m.put(key, v);
            }
        } finally {
            log.info("{}写锁释放 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
            w.unlock();
        }
        return v;
    }

_5

原创声明:本文来源于微信公众号【胖滚猪学编程】,持续更新JAVA大数据干货,用漫画形式让编程so easy and interesting。转载请注明出处。

ReentrantReadWriteLock的特点功能

J.U.C Lock包之ReentrantLock互斥锁,咱们介绍了ReentrantLock相比synchronized的几大特点功能,例如公平锁、非阻塞获取锁、超时、中断。那么ReentrantReadWriteLock是否也有呢?

简单。。看看源码不就清楚了。如下源码都是在ReentrantReadWriteLock.java中撩出来的~ 剩下的我就不用多说了吧!若是不清楚这些方法能够回头看看 J.U.C Lock包之ReentrantLock互斥锁

public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }
public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }

读写锁的升级与降级

还想跟你聊聊锁的升级和降级。也许你是第一次听到,锁还有升级降级的功能。但其实不难理解,好比在读写锁中,写锁变为读锁是彻底可行的方案,不会有任何问题,这里写锁变读锁就叫作锁的降级

那么能够升级吗?熟话说降级容易,你只要每天不来上班就好了,升级可难哦。锁中也是,只是在锁中更加苛刻,彻底不容许升级,即读锁没法升级为写锁必须先释放读锁,才能够获取写锁。为何不容许升级?试想有1000个读线程同时执行,同时升级为写锁,会发生什么?获取写锁的前提是读锁和写锁均未被占用,所以可能致使阻塞较长的时间,也可能发生死锁。

先写个代码验证一下吧,在(2)处咱们实现了降级,程序是彻底ok的,在(1)处若是你注释掉 r.unlock(),试图升级为读锁,你会发现程序会跑不下去的,据此能够验证咱们所说的:读写锁能够降级、没法升级。

void processCachedData() {
        // 获取读锁
        r.lock();
        if (!cacheValid) {
            // 释放读锁 由于不容许读锁的升级 能够注释掉该行代码 整个程序会阻塞
            r.unlock(); //(1)
            // 获取写锁
            w.lock();
            try {
                // 再次检查状态
                if (!cacheValid) {
                    data = "胖滚猪学编程";
                    cacheValid = true;
                }

                // 释放写锁前 降级为读锁 降级是能够的
                r.lock(); //(2)
            } finally {
                // 释放写锁
                w.unlock();

            }

        }
        // 此处仍然持有读锁
        try {
            System.out.println(data);
        } finally {
            r.unlock();
        }

    }

总结

读写锁适用于读多写少的状况。能够优化性能,提高易用性。缓存就是个很好的例子。

读写锁最大的特征是容许多个线程同时读共享变量。可是只容许一个线程写共享变量,且若是一个写线程正在执行写操做,此时禁止读线程读共享变量。

ReentrantReadWriteLock读写锁相似于 ReentrantLock,支持公平模式和非公平模式、支持非阻塞获取锁、超时、中断等特性。可是有一点须要注意,那就是只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出 UnsupportedOperationException 异常。

因此!咱们必须了解各类锁的用途,才能在生产上选择最合适高效的方式。

原创声明:本文来源于微信公众号【胖滚猪学编程】,持续更新JAVA大数据干货,用漫画形式让编程so easy and interesting。转载请注明出处。
相关文章
相关标签/搜索