• 17---读写锁ReadwriteLock——实现一个完备的缓存


    管程和信号量这两个同步原语在 Java 语言中的实现,理论上用这两个同步原语中任何一个都可以解决所有的并发问题。那 Java SDK 并发包里为什么还有很多其他的工具类呢?原因很简单:分场景优化性能,提升易用性。

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

    一、读写锁都遵守以下三条基本原则:

    • 允许多个线程同时读共享变量;
    • 只允许一个线程写共享变量;
    • 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
    • 只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

    最后一点可以反过来想,假如一个线程获取了读锁,而另外一个线程获取了写锁,那不是写的同时还可以读了吗,这与第三点矛盾了。

    性能优于互斥锁的原因:允许多个线程同时读共享变量。

    二、实现一个简单的缓存(按需加载)

     1 class Cache<K,V> {
     2   final Map<K, V> m =
     3     new HashMap<>();
     4   final ReadWriteLock rwl = 
     5     new ReentrantReadWriteLock();
     6   final Lock r = rwl.readLock();
     7   final Lock w = rwl.writeLock();
     8  
     9   V get(K key) {
    10     V v = null;
    11     //读缓存
    12     r.lock();         ①
    13     try {
    14       v = m.get(key); ②
    15     } finally{
    16       r.unlock();     ③
    17     }
    18     //缓存中存在,返回
    19     if(v != null) {   ④
    20       return v;
    21     }  
    22     //缓存中不存在,查询数据库
    23     w.lock();         ⑤
    24     try {
    25       //再次验证
    26       //其他线程可能已经查询过数据库
    27       v = m.get(key); ⑥
    28       if(v == null){  ⑦
    29         //查询数据库
    30         v=省略代码无数
    31         m.put(key, v);
    32       }
    33     } finally{
    34       w.unlock();
    35     }
    36     return v; 
    37   }
    38      
    39    // 写缓存  
    40    void put(K key, V value) {   
    41         w.lock();    
    42          try {
    43               m.put(key, v); 
    44          }finally { w.unlock(); }  }
    45 }
    View Code

    这个缓存没有解决缓存数据与源头数据的同步问题,这里的数据同步指的是保证缓存数据和源头数据的一致性。解决数据同步问题的一个最简单的方案就是超时机制。所谓超时机制指的是加载进缓存的数据不是长久有效的,而是有时效的,当缓存的数据超过时效,也就是超时之后,这条数据在缓存中就失效了。而访问缓存中失效的数据,会触发缓存重新从源头把数据加载进缓存。

    还有一些方案采取的是数据库和缓存的双写方案。具体看场景。

    三、锁的升降级

     1 //读缓存
     2 r.lock();         ①
     3 try {
     4   v = m.get(key); ②
     5   if (v == null) {
     6     w.lock();
     7     try {
     8       //再次验证并更新缓存
     9       //省略详细代码
    10     } finally{
    11       w.unlock();
    12     }
    13   }
    14 } finally{
    15   r.unlock();     ③
    16 }
    View Code

    在上面的代码示例中,先是获取读锁,然后再升级为写锁,对此还有个专业的名字,叫锁的升级。可惜 ReadWriteLock 并不支持这种升级。在上面的代码示例中,读锁还没有释放,此时获取写锁,会导致写锁永久等待,最终导致相关线程都被阻塞,永远也没有机会被唤醒。锁的升级是不允许的。

     1 class CachedData {
     2   Object data;
     3   volatile boolean cacheValid;
     4   final ReadWriteLock rwl =
     5     new ReentrantReadWriteLock();
     6   // 读锁  
     7   final Lock r = rwl.readLock();
     8   //写锁
     9   final Lock w = rwl.writeLock();
    10   
    11   void processCachedData() {
    12     // 获取读锁
    13     r.lock();
    14     if (!cacheValid) {
    15       // 释放读锁,因为不允许读锁的升级
    16       r.unlock();
    17       // 获取写锁
    18       w.lock();
    19       try {
    20         // 再次检查状态  
    21         if (!cacheValid) {
    22           data = ...
    23           cacheValid = true;
    24         }
    25         // 释放写锁前,降级为读锁
    26         // 降级是可以的
    27         r.lock(); ①
    28       } finally {
    29         // 释放写锁
    30         w.unlock(); 
    31       }
    32     }
    33     // 此处仍然持有读锁
    34     try {use(data);} 
    35     finally {r.unlock();}
    36   }
    37 }
    View Code

    But,锁的降级是允许的。如上的代码这种锁的降级是支持的。

    四、注意

    1.有一点需要注意,那就是只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出 UnsupportedOperationException 异常。

    2.读写锁类似于 ReentrantLock,也支持公平模式和非公平模式。读锁和写锁都实现了 java.util.concurrent.locks.Lock 接口,所以除了支持 lock() 方法外,tryLock()、lockInterruptibly() 等方法也都是支持的。

    五、思考

    有同学反映线上系统停止响应了,CPU 利用率很低,你怀疑有同学一不小心写出了读锁升级写锁的方案,那你该如何验证自己的怀疑呢?

    1. 源代码分析。查找ReentrantReadWriteLock在项目中的引用,看下写锁是否在读锁释放前尝试获取
    2. 如果线上是Web应用,应用服务器比如说是Tomcat,并且开启了JMX,则可以通过JConsole等工具远程查看下线上死锁的具体情况

  • 相关阅读:
    Android_listview设置每条信息的间距
    Android实现ListView或GridView首行/尾行距离屏幕边缘距离
    实现类似微信的延迟加载的Fragment——LazyFragment
    struts2的Action该方法不能去
    (工具)source insight高速增加时间代码
    猫学习IOS(十五)UI以前的热的打砖块游戏
    java语言内部类和匿名内部类
    JVM截至多少线程可以创建: unable to create new native thread
    linux下一个Oracle11g RAC建立(八)
    转基因小麦--主题在农业科技的最前沿
  • 原文地址:https://www.cnblogs.com/bbsh/p/11733666.html
Copyright © 2020-2023  润新知