
在今天的开发调试当中,一个多线程的项目出现了泛型死锁的难题。按照正常的缓解策略,上监控软件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机制。同时也体现了对第三方代码没有细读。
本文来自电脑杂谈,转载请注明本文网址:
https://www.pc-fly.com/a/jisuanjixue/article-120754-1.html
再多的叙利亚人也容得下
你的老师就教你百度了