`
vanadiumlin
  • 浏览: 493473 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

关于ReentrantReadWriteLock两个问题及解决心得(转)

 
阅读更多

前些时候写了一个简单的cache类,实现了一些基本的功能:get和update操作。由于使用在多线程的环境中,为了提高效率,我采用了ReadWriteLock,毕竟我这里缓存更新少,取数据多。特别是一些timeout为-1的数据(持久数据),添加进去之后基本不会被更新。这种场景用读写锁是自然而然的事情(由于时间比较紧,也没去花时间了解有没有什么更适合的锁)。
    缓存实现中
        public V get(K key){
            readLock.lock();
            try{
                 CacheObject value = map.get(key);
                if(!value.enable()){
                   remove(key);
                    return null;
                } else {
                    return value.getObject();
                }
            } catch(Exception e){return null;}
            finally {
                readLock.unlok();
            }
        }
 
        private boolean remove(K key){
            writeLock.lock();
            try{
                map.remove(key);
                return true;
            } catch(Exception e){return false;}
            finally{
                writeLock.unlock();
            }
        }
    大概的get逻辑就是这个了。开始我也没有在意这个同步的问题,写完了看了两眼觉得应该没什么问题了。然后今天在测试功能的时候,觉得有点奇怪。在取监控数据的时候,怎么没有提示呢?每次服务器重启完第一次是可以的,有时候可能正常一次,可能正常两次或者三次,然后就开始卡死再也不出数据。每次遇到这种奇怪的时候我的神经就莫名奇妙的兴奋。
    开始我怀疑是去远程调用其他服务的时候出问题,网络有问题,因为我发现我点击没有配置不用去远程取数据的那些数据都是正常的,当然只能是猜测,线上的服务器连不上去。我只能去线上抓数据,拿下来本地调试。发现不是远程取数据的问题,却在取缓存数据的时候停住了。我看了好几遍这个get方法,看来看去也没问题啊。看自己的代码很难能看出什么bug,所以说单元测试不能自己写完代码之后再写单元测试呢。写个程序来测试这个缓存的get操作吧。
    很简单的new一个cache对象,然后每隔1秒钟调用一次get方法。哎,还真的三次之后就卡住了,这下确定了错误的原因,再继续跟第三次的get,到remove的时候卡住了,我猛然就明白了,靠,死锁了。
    我设置的缓存的超时时间是3秒钟,然后第三次去取的时候已经超时,需要remove旧数据,在get操作的时候当前线程已经获取了读锁,然后remove的时候又尝试去获取写锁,读锁和写锁冲突了,然后就你等我我等你,就死了。
    
    后来看了下ReentrantReadWriteLock的javaodoc,发现Doug Lea大师已经知道我们这种小搓搓写cache的时候肯定要用到这个,还特意写了个例子:
         class CachedData {
   Object data;
   volatile boolean cacheValid;
   ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 
   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
        // Must release read lock before acquiring write lock
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        // Recheck state because another thread might have acquired
        //   write lock and changed state before we did.
        if (!cacheValid) {
          data = ...
          cacheValid = true;
        }
        // Downgrade by acquiring read lock before releasing write lock
        rwl.readLock().lock();
        rwl.writeLock().unlock(); // Unlock write, still hold read
     }
 
     use(data);
     rwl.readLock().unlock();
   }
 }
    大师就是不一样啊,提前知道我们在想什么...
    我在仔细看了下Doug Lea大师怎么好像写错了,他加锁的顺序:
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        rwl.readLock().lock();
        rwl.writeLock().unlock(); 
        rwl.readLock().unlock();
    先获取了写锁,还没释放就开始拿读锁,这不也是死锁吗?动手就知道了,写了个测试代码,咦,怎么没有死锁。继续看javadoc,找到了不死锁的原因,原来ReentrantReadWriteLock有锁降级机制,写锁是exclusive锁,读锁是share锁,如果一个线程拿到了写锁,那么它还可以继续拿到读锁,降级是我自己说的,可能不准确,应该能明白我的意思。
    后来想想还是不对,为了要有这么个机制,这样有什么好处吗?让我开始还理解错了,跟我师兄争论了好久。晚上洗澡的时候突然想到:这个降级的机制可能保证缓存中get操作的场景的原子性。
    怎么说?这个说起来有点复杂,挑最简单的被动失效缓存举例子。我想get操作的时候,发现数据已经失效,需要用新数据覆盖失效数据,并返回最新结果。这个场景用锁降级可以保证。当然用synchronized可以实现,只是那样的话就用不到读写锁的读读不互斥的优点了(转入正题)。
    用大师的这个加锁顺序:
        rwl.writeLock().lock();
        rwl.readLock().lock();
        rwl.writeLock().unlock(); 
        rwl.readLock().unlock();
    看出来了没?如果一个线程拿到了写锁,即使更新缓存的动作做完了,再拿到读锁,准备返回数据。这样可以保证返回的数据是这次更新的数据。如果没有这个降级机制的话,在释放了读锁之后,可能会被另外一个线程抢到读锁,继续更新缓存,再返回的数据就不是第一次更新的数据了。
    或许这个机制是专门为了缓存设计的?其实我想可能还有别的原因,场景可能还没遇到,后面遇到可能就明白了。
 
    看完这篇文章,写了两个问题:
    (1)使用ReadWriteLock时,一种产生死锁的动作。
    (2)ReentrantReadWriteLock的降级机制,加锁顺序有嚼头。这个还没理解透,大家有什么好的例子或者心得?

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics