我有一个庞大的Trove地图和一个我需要经常从多个线程调用的方法。大多数情况下,此方法应返回 true 。线程正在进行大量的运算,我注意到由于以下方法存在一些争用(这只是一个例子,我的实际代码有点不同):
synchronized boolean containsSpecial() {
return troveMap.contains(key);
}
请注意,这是一个“仅附加”地图:一旦添加了一个键,就会永远停留在那里(这对我接下来的事情很重要)。
我注意到通过将上面改为:
boolean containsSpecial() {
if ( troveMap.contains(key) ) {
// most of the time (>90%) we shall pass here, dodging lock-acquisition
return true;
}
synchronized (this) {
return troveMap.contains(key);
}
}
我的数字运算得到了20%的加速(经过多次运行验证,长时间运行等)。
这种优化看起来是否正确(知道一旦钥匙存在,它将永远留在那里)?
这项技术的名称是什么?
修改
更新地图的代码比 containsSpecial()方法更频繁地调用,看起来像这样(我已经同步了整个方法):
synchronized void addSpecialKeyValue( key, value ) {
....
}
答案 0 :(得分:6)
它被称为错误锁定;-)实际上,它是双重检查锁定方法的一些变体。而这种方法的原始版本在Java中是完全错误的。
允许Java线程在其本地内存中保留变量的私有副本(想想:多核机器的核心本地缓存)。除非发生某些同步,否则允许任何Java实现永远不会将更改写回全局内存。
因此,您的一个线程很可能具有troveMap.contains(key)
评估为true
的本地内存。因此,它永远不会同步,也永远不会获得更新的内存。
此外,当contains()
看到troveMap数据结构的内存不一致时会发生什么?
查找Java内存模型以获取详细信息。或者看看这本书:Java Concurrency in Practice。
答案 1 :(得分:6)
此代码不正确。
Trove不处理并发使用本身;在这方面就像java.util.HashMap
。因此,像HashMap
一样,即使看似无辜的只读方法(如containsKey()
)也可能抛出运行时异常,或者更糟糕的是,如果另一个线程同时修改了映射,则进入无限循环。我不知道Trove的内部,但是使用HashMap
,在超出加载因子时重新发送,或者删除条目会导致其他只读取的线程出现故障。
如果与锁管理相比,操作需要大量时间,使用read-write lock消除序列化瓶颈将大大提高性能。在ReentrantReadWriteLock
的课程文档中,有“示例用法”;您可以使用第二个示例RWDictionary
作为指南。
在这种情况下,映射操作可能非常快,锁定开销占主导地位。如果是这种情况,您需要在目标系统上进行概要分析,以查看synchronized
块或读写锁是否更快。
无论哪种方式,重要的一点是您不能安全地删除所有同步,否则您将遇到一致性和可见性问题。
答案 2 :(得分:2)
这对我来说似乎不安全。具体来说,未同步的调用将能够看到部分更新,这可能是由于内存可见性(之前的put没有完全发布,因为你还没有告诉JMM它需要)或者是由于一个普通的老种族。想象一下,如果TroveMap.contains
有一些内部变量,它假设在contains
期间不会改变。此代码允许不变的中断。
关于内存可见性,问题不是假阴性(你使用同步的双重检查),但可能违反了这个不相关的不变量。例如,如果他们有一个计数器,并且他们始终需要counter == someInternalArray.length
,那么缺少同步可能会违反该计数器。
我的第一个想法是制作troveMap的引用volatile
,并在每次添加到地图时重新编写引用:
synchronized (this) {
troveMap.put(key, value);
troveMap = troveMap;
}
这样,你就设置了一个内存屏障,这样任何阅读troveMap
的人都可以保证在最近的任务之前看到发生的所有事情 - 也就是说,它的最新状态。这解决了内存问题,但它无法解决竞争条件。
根据您的数据更改速度,Bloom filter可能会有所帮助吗?还是其他一些针对某些快速路径更优化的结构?
答案 3 :(得分:1)
我认为你最好使用ConcurrentHashMap,它不需要显式锁定并允许并发读取
boolean containsSpecial() {
return troveMap.contains(key);
}
void addSpecialKeyValue( key, value ) {
troveMap.putIfAbsent(key,value);
}
另一个选项是使用ReadWriteLock,它允许并发读取但不允许并发写入
ReadWriteLock rwlock = new ReentrantReadWriteLock();
boolean containsSpecial() {
rwlock.readLock().lock();
try{
return troveMap.contains(key);
}finally{
rwlock.readLock().release();
}
}
void addSpecialKeyValue( key, value ) {
rwlock.writeLock().lock();
try{
//...
troveMap.put(key,value);
}finally{
rwlock.writeLock().release();
}
}
答案 4 :(得分:1)
在您描述的条件下,很容易想象一个地图实现,您可以通过无法同步来获得漏报。我可以想象得到误报的唯一方法是一种实现,其中键插入是非原子的,部分键插入看起来像你正在测试的另一个键。
您没有说明您实施的是哪种地图,但是库存地图实施通过分配参考来存储密钥。根据J ava Language Specification:
对引用的写入和读取始终是原子的,无论它们是实现为32位还是64位值。
如果您的地图实现使用对象引用作为键,那么我看不出您是如何遇到麻烦的。
修改强>
以上内容是因为对Trove本身的无知而写的。经过一番研究后,我发现Rob Eden(Trove的开发者之一)the following post关于Trove地图是否并发:
Trove不会修改检索的内部结构。但是,这是一个实现细节而不是保证,所以我不能说它在将来的版本中不会改变。
所以看起来这种方法现在可行,但在将来的版本中可能根本不安全。尽管受到了惩罚,最好使用Trove的同步地图类之一。
答案 5 :(得分:0)
为什么要重新发明轮子? 只需使用ConcurrentHashMap.putIfAbsent
即可