使用guava Striped中的lock导致线程死锁的问题分析

2016/11/19 15:04:00 No Comments

在最近的开发调试当中,一个多线程的项目出现了线程死锁的问题。按照正常的解决思路,上监控工具yourkit或者自己打印出线程信息,最终的显示信息均为lock对象先拿到了自己的锁,然后去拿对方的锁,现状是这样的。但与一些正常的项目相比,按照正常的执行情况,是不可能出现死锁的。

项目使用了程序锁来保证并发情况下,一些操作只能由单个来执行,如一些不允许多次运行的场景。所谓程序锁,即通过由锁池获取相应的lock对象,然后进行lock操作,在相应的方法体内进行相应的控制。
项目中使用基于guava的striped来获取不同的锁,本来原来的代码是自己封装实现,但考虑到guava已经提供,因此直接使用了,相应的伪代码如下所示:

        Striped<Lock> lockStriped = Striped.lazyWeakLock(1024);

        Lock lock = lockStriped.get("动态生成的临界资源标识符");
        lock.lock();
        try{
            //执行业务操作
        } finally {
            lock.unlock();
        }

以上的代码因为足够的通用,因此被封装为一个LockAdvice,以实现通用的锁方法操作,只需要在需要加锁的方法上加上Lockable注解,就可以达到所要的效果。出现死锁的2个线程表现如下:

//线程1
加锁获取资源类型1
加锁获取资源类型2

//线程2
加锁获取资源类型1
加锁获取类型类型2

从上面的线程操作来看,加锁的顺序是一样的,释放锁的顺序也一致,因此理论上不会出现死锁。为保证锁切面的简单实现,上面的锁资源类型1 被表现为一个字符串,即通过不同的字符串来进行描述。也就是说,在上面的现象中,会有4个字符串进行锁的key进行操作。

问题场景被发现了,并且这个问题肯定会产生,多线程运行一段时间之后就会发生。查找问题产生的原因却很漫长。不知道为什么,使用jstack却没有打印出每个线程在哪一个环节持有哪个锁(是不是本身就没有),通过jstack的信息表示,相应的线程肯定持有对方的锁,而对方也在等级已方的锁,并且相应的锁的发生点也确定是在相应的锁切面准备执行的时候。剩下的问题就是看是否是相应的锁生成器striped本身就返回了相同的锁,如果由锁生成器本身就返回相同的锁,那么就肯定有问题了。

打开guava的代码,查看相应的striped的get(String)方法是如何实现的,看到了如下的实现方式:

private abstract static class PowerOfTwoStriped<L> extends Striped<L> {
    @Override final int indexFor(Object key) {
      int hash = smear(key.hashCode());
      return hash & mask;
    }

    @Override public final L get(Object key) {
      return getAt(indexFor(key));
    }
  }

//相应的子类
  @VisibleForTesting static class LargeLazyStriped<L> extends PowerOfTwoStriped<L> {

    @Override public L getAt(int index) {
      if (size != Integer.MAX_VALUE) {
        Preconditions.checkElementIndex(index, size());
      } // else no check necessary, all index values are valid
      L existing = locks.get(index);
      if (existing != null) {
        return existing;
      }
      L created = supplier.get();
      existing = locks.putIfAbsent(index, created);
      return MoreObjects.firstNonNull(existing, created);
    }

其实第一眼看到它的实现将相应的key转换为一个下标之后,就不再使用时,就感觉不对了。因为下标肯定是会重复的。

上面的代码是通过以下的构造方式产生的,这也是程序所使用的通用方式:

Striped<Lock> lockStriped = Striped.lazyWeakLock(1024 * 2);

再回头看相应的guava的所谓indexFor方法,也就是通过hash算出相应的hash值,再和相应的存储长度进行&运算,在这种情况下,不同的key有很大的机率会落到同一个桶中。这样本来应该拿key1的锁信息,却把key2的锁给获取到,这样肯定会产生死锁。

问题的解决方式也很简单,不再使用striped对象,而使用由jdk中concurrenthashmap所实现的,并且在相应的striped中,guava也是采用了concurrenthashmap来实现分组锁信息。相应的实现如下所示。

//1.8 之前的实现方式,直接copy自guava
//如果原来有,就直接返回
L existing = locks.get(index);
      if (existing != null) {
        return existing;
      }
//这里进行新建
      L created = supplier.get();
      existing = locks.putIfAbsent(index, created); //这里必须使用putIfAbsent,即原来没有才放入,以保证不会出现锁丢失
//如果原来有(可能是另一个线程放入的),就仍然返回原来有的,否则才返回自己创建的
      return MoreObjects.firstNonNull(existing, created);

//可以看出1.8之前的实现比较复杂和繁琐,在1.8之后,提供了一个很简单的方式,如下所示:
lockStriped.computeIfAbsent(key, t -> new ReentrantLock())

通过1.8的代码,在使用相应的computeIfAbsent,即保证了相应的本身实现的多线程锁处理,又避免了会创建多个lock的情况。

后记:出现此问题的原因还是对第三方代码太过相信了,感觉像是和自己的场景相同,但实际上用了才知道。如果striped使用于不同的资源类型使用不同的striped情况下,相应的死锁问题说不定也不会发生,这也跟具体的使用场景相关。在我们的项目中,所有的资源类型共用同一个字符串类型,仅通过字符串本身的equals机制进行保证,但偏偏striped放弃了对equals的支持而采用idx机制。同时也反映了对第三方代码没有细读。

转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201611190001.html

相关文章:

留下足迹