保持“明显”锁定检索或采用双重检查锁定?

时间:2011-03-08 14:32:24

标签: java optimization concurrency race-condition

我很狡猾地提出问题。我有以下(Java)代码(伪):

public SomeObject getObject(Identifier someIdentifier) {
    // getUniqueIdentifier retrieves a singleton instance of the identifier object,
    // to prevent two Identifiers that are equals() but not == (reference equals) in the system.
    Identifier singletonInstance = getUniqueIdentifier(someIdentifier);
    synchronized (singletonInstance) {
        SomeObject cached = cache.get(singletonInstance);
        if (cached != null) {
            return cached;
        } else {
            SomeObject newInstance = createSomeObject(singletonInstance);
            cache.put(singletonInstance, newInstance);
            return newInstance;
        }
    }
}

基本上,它使标识符'唯一'(引用等于,如在==中),检查缓存,并且在高速缓存未命中的情况下,调用昂贵的方法(涉及调用外部资源和解析,等),将其放入缓存中,然后返回。在这种情况下,同步Identifier会避免使用两个equals()但不会== Identifier个对象来调用昂贵的方法,这会同时检索相同的资源。

以上作品。我只是想知道,并且可能是微优化,重写如下,采用更天真的缓存检索和双重检查锁定是'安全的'(在线程安全中是安全的,没有奇怪的竞争条件)并且是'更多最佳'(如减少不需要的锁定和线程必须等待锁定)?

public SomeObject getObject(Identifier someIdentifier) {

    // just check the cache, reference equality is not relevant just yet.
    SomeObject cached = cache.get(someIdentifier);
    if (cached != null) {
        return cached;
    }        

    Identifier singletonInstance = getUniqueIdentifier(someIdentifier);
    synchronized (singletonInstance) {
        // re-check the cache here, in case of a context switch in between the 
        // cache check and the opening of the synchronized block.
        SomeObject cached = cache.get(singletonInstance);
        if (cached != null) {
            return cached;
        } else {
            SomeObject newInstance = createSomeObject(singletonInstance);
            cache.put(singletonInstance, newInstance);
            return newInstance;
        }
    }
}

你可以说“只是测试它”或“只做一个微基准测试”,但测试多线程代码不是我的强项,我怀疑我能够模拟真实情况或准确假的竞争条件。加上它需要我半天,而写一个SO问题只需要几分钟:)。

3 个答案:

答案 0 :(得分:0)

同步最多需要2微秒。除非你需要进一步削减,否则最简单的解决方案可能会更好。

BTW你可以写

SomeObject cached = cache.get(singletonInstance);
if (cached == null) 
   cache.put(singletonInstance, cached = createSomeObject(singletonInstance));
return cached;

答案 1 :(得分:0)

如果“cache”是一个地图(我怀疑它是),那么这个问题与简单的双重检查锁定问题完全不同。

如果缓存是普通的HashMap,那么问题实际上要糟糕得多;即你提出的“双重检查模式”比简单的基于参考的双重检查更糟糕。实际上,它可能导致ConcurrentModificationExceptions,获取不正确的值,甚至是无限循环。

如果它基于普通的HashMap,我建议使用ConcurrentHashMap作为第一种方法。使用ConcurrentHashMap,您无​​需显式锁定。

public SomeObject getObject(Identifier someIdentifier) {
    // cache is a ConcurrentHashMap

    // just check the cache, reference equality is not relevant just yet.
    SomeObject cached = cache.get(someIdentifier);
    if (cached != null) {
        return cached;
    }        

    Identifier singletonInstance = getUniqueIdentifier(someIdentifier);
    SomeObject newInstance = createSomeObject(singletonInstance);
    SombObject old = cache.putIfAbsent(singletonInstance, newInstance);
    if (old != null) {
        newInstance = old;
    }
    return newInstance;
}

答案 2 :(得分:0)

您正在重塑Google-Collections / Guava的MapMaker / ComputingMap:

ConcurrentMap<Identifier, SomeObject> cache = new MapMaker().makeComputingMap(new Function<Identifier, SomeObject>() {
  public SomeObject apply(Identifier from) {
    return createSomeObject(from);
  }
};

public SomeObject getObject(Identifier someIdentifier) {
  return cache.get(someIdentifier);
}

此处不需要实习,因为ComputingMap保证单个线程只会在缺席时尝试填充,而另一个要求同一项的线程将阻塞并等待结果。如果删除正在填充的密钥,那么该线程和当前正在等待的任何密钥仍将获得该结果,但后续请求将再次启动填充。

如果你确实需要实习,那么该库提供了优秀的Interner类,它具有强引用和弱引用的缓存。