新版博客升级完成

一个使用本地缓存引起的线程阻塞问题

现象

有同事的java系统运行一段时间后发生请求阻塞的情况(返回504),从仅有的内存dump文件看,大部分线程都阻塞在了一个本地缓存(jodd cache)的读锁上了(ReentrantReadWriteLock$ReadLock.lock)。

排查过程

阶段一

本能的反应应该是写锁被占用了才会出现这个情况。于是开始以"WriteLock.lock"为关键字搜索写锁,怎么也搜不到。其实搜不到是正常的,因为写锁已经被占有了,当然不可能停在WriteLock.lock上了。

开始翻jodd LRUCache代码,发现是用LinkedHashMap实现的,在dump文件上搜索LinkedHashMap写操作的代码,果然发现有一个线程是正在执行LRUCache的put方法,代码停留在LRUCache的pruneCache方法中(就是在put的时候cache满了回收一些位置):

protected int pruneCache() {
    if (isPruneExpiredActive() == false) {
        return 0;
    }
    int count = 0;
    //cacheMap就是一个LinkedHashMap的实例
    Iterator<CacheObject<K,V>> values = cacheMap.values().iterator();
    while (values.hasNext()) {
        CacheObject<K,V> co = values.next();
        if (co.isExpired() == true) {
            values.remove();
            count++;
        }
    }
    return count;
}
    

到这里就证明了最初的猜想是对的,写锁被占了才导致那么多读线程被堵住。

可以看出 jodd 使用 LinkedHashMap + ReentrantReadWriteLock 实现LRUCache是有性能问题的,一个写操作会锁住整个缓存,阻塞所有读操作。这是第一个问题

阶段二

显然不能到此就结束了,要有更高的追求,继续分析LRUCache的具体实现,主要逻辑就是put时加上写锁,get时加上读锁,内部是一个开启了accessOrder的LinkedHashMap作为数据存储。

初看也貌似很正常没啥问题啊。其实开启了accessOrder的LinkedHashMap 多线程get是会有并发问题的,因为会把get到的元素移到双向链表最前面,看LinkedHashMap的get方法:

public V get(Object key) {
    Entry<K,V> e = (Entry<K,V>)getEntry(key);
    if (e == null)
        return null;
    e.recordAccess(this);
    return e.value;
} 

void recordAccess(HashMap<K,V> m) {
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    if (lm.accessOrder) {
        lm.modCount++;
        remove();
        addBefore(lm.header);
    }
}

可以看到这里改变链表结构是没有任何并发控制的,因此LinkedHashMap并发get是不OK的,jodd给get加了读锁是存在并发问题的(还不明白的请自行学习ReentrantReadWriteLock机制)。这是第二个问题

可以想象下高并发时链表被破坏成各种奇形怪状的情况(比较费脑力,我就不描述了),完全有可能让上面pruneCache()方法中的values.hasNext()永远为true。这次刚好是停在LRUCache#pruneCache中,下次就有可能停在LinkedHashMap#transfer上,一旦写锁里面的代码块hang住,所有读线程全部堵住,而且这种问题出现几率不等,很难模拟重现。

JUC Bug

另外顺便提一下某些早期JDK版本中存在的BUG

ReentrantReadWriteLock可能在没有任何线程持有锁的情况下被hang住:
http://bugs.sun.com/view_bug.do?bug_id=6822370
http://bugs.sun.com/view_bug.do?bug_id=6903249

小结

  • 不要使用Jodd的cache
  • 推荐使用gauva的cache
    基于concurrentlinkedhashmap实现,现已整合到guava里了
  • 不可轻信开源组件,使用前一定要先研究透彻

本文永久链接: http://jenwang.me/14853486232734.html


进一步交流:
- Email:jenwang@foxmail.com
- 对于本博客某些话题感兴趣,希望进一步交流的,请加 qq 群:2825967
- 更多技术交流分享在圈子「架构杂谈」,跟老司机们聊聊互联网前沿技术、架构、工具、解决方案等