• 多线程编程核心技术(十三)ReadWriteLock


    一种非常普遍的并发场景:读多写少场景。实际工作中,为了优化性能,我们经常会使用缓存,例如缓存元数据、缓存基础数据等,这就是一种典型的读多写少应用场景。缓存之所以能提升性能,一个重要的条件就是缓存的数据一定是读多写少的,例如元数据和基础数据基本上不会发生变化(写少),但是使用它们的地方却很多(读多)。

    不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。

    这个stamp的作用从源码中看是用来进行校验的,但如果是作为校验的标志位,本身线程的方法栈的地址就可以当票据了。这个我也想不明白,如果是锁升级的原因,那进行对象更新就行了。

    public void unlockWrite(long stamp) {
            WNode h;
            if (state != stamp || (stamp & WBIT) == 0L)
                throw new IllegalMonitorStateException();
            state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
            if ((h = whead) != null && h.status != 0)
                release(h);
        }
    

      

    读写锁,并不是 Java 语言特有的,而是一个广为使用的通用技术,所有的读写锁都遵守以下三条基本原则:

    1.允许多个线程同时读共享变量;

    2.只允许一个线程写共享变量;

    3.如果一个写线程正在执行写操作,此时禁止读线程读共享变量。

    读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。

    class Cache<K,V> {
      final Map<K, V> m =
        new HashMap<>();
      final ReadWriteLock rwl = 
        new ReentrantReadWriteLock();
      final Lock r = rwl.readLock();
      final Lock w = rwl.writeLock();
     
      V get(K key) {
        V v = null;
        //读缓存
        r.lock();         ①
        try {
          v = m.get(key); ②
        } finally{
          r.unlock();     ③
        }
        //缓存中存在,返回
        if(v != null) {   ④
          return v;
        }  
        //缓存中不存在,查询数据库
        w.lock();         ⑤
        try {
          //再次验证
          //其他线程可能已经查询过数据库
          v = m.get(key); ⑥
          if(v == null){  ⑦
            //查询数据库
            v=省略代码无数
            m.put(key, v);
          }
        } finally{
          w.unlock();
        }
        return v; 
      }
    }

      获取写锁的前提是读锁和写锁均未被占用 获取读锁的前提是没有其他线程占用写锁。所以想更新缓存,需要再次调用写方法,如果直接在read方法内部对锁升级为write锁就会导致死锁。

    但是write锁是可以进行降级为read锁的,因为写锁的特点是同时需要读锁和写锁。

    class CachedData {
      Object data;
      volatile boolean cacheValid;
      final ReadWriteLock rwl =
        new ReentrantReadWriteLock();
      // 读锁  
      final Lock r = rwl.readLock();
      //写锁
      final Lock w = rwl.writeLock();
      
      void processCachedData() {
        // 获取读锁
        r.lock();
        if (!cacheValid) {
          // 释放读锁,因为不允许读锁的升级
          r.unlock();
          // 获取写锁
          w.lock();
          try {
            // 再次检查状态  
            if (!cacheValid) {
              data = ...
              cacheValid = true;
            }
            // 释放写锁前,降级为读锁
            // 降级是可以的
            r.lock(); ①
          } finally {
            // 释放写锁
            w.unlock(); 
          }
        }
        // 此处仍然持有读锁
        try {use(data);} 
        finally {r.unlock();}
      }
    }

    读写锁类似于 ReentrantLock,也支持公平模式和非公平模式。读锁和写锁都实现了 java.util.concurrent.locks.Lock 接口,所以除了支持 lock() 方法外,tryLock()、lockInterruptibly() 等方法也都是支持的。但是有一点需要注意,那就是只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出 UnsupportedOperationException 异常。

    另外这里的读写锁,性能还有可以提升的地方,因为可能很多业务都会使用这个缓存懒加载,实际生产环境,写缓存操作可能会比较多,那么不同的缓存key,实际上是没有并发冲突的,所以这里的读写锁可以按key前缀拆分,即使是同一个key,也可以类似ConcurrentHash 一样分段来减少并发冲突

  • 相关阅读:
    第九篇:网络编程
    第十篇:并发编程
    Python-GIL 进程池 线程池
    Python-生产者消费模型 线程
    Python-互斥锁 进程间通讯
    第八篇:异常处理
    第六篇:面向对象
    第四篇:模块与包
    【转】英语中的并列连词,只知道 and 和 but?11组并列连词,一篇搞定!
    【转】英语中的从属连词,28个,一篇搞定(句子结构2)
  • 原文地址:https://www.cnblogs.com/SmartCat994/p/14210339.html
Copyright © 2020-2023  润新知