这种锁定技术的名称是什么?

时间:2011-12-07 17:54:06

标签: java optimization synchronization locking trove4j

我有一个庞大的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 ) {
    ....
}

6 个答案:

答案 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

即可