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

时间:2022-09-16 06:58:39

I suck at formulating questions. I have the following piece of (Java) code (pseudo):

我很狡猾地提出问题。我有以下(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;
        }
    }
}

Basically, it makes an identifier 'unique' (reference equals, as in ==), checks a cache, and in case of a cache miss, calls an expensive method (involving calling an external resource and parsing, etc), puts that in the cache, and returns. The synchronized Identifier, in this case, avoids two equals() but not == Identifier objects being used to call the expensive method, which would retrieve the same resource simultaneously.

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

The above works. I'm just wondering, and probably micro-optimizing, would a rewrite such as the following that employs more naïve cache retrieval and double-checked locking be 'safe' (safe as in threadsafe, void of odd race conditions) and be 'more optimal' (as in a reduction of unneeded locking and threads having to wait for a lock)?

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

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;
        }
    }
}

You could say 'Just test it' or 'Just do a micro-benchmark', but testing multi-threaded bits of code isn't my strong point, and I doubt I'd be able to simulate realistic situations or accurately fake race conditions. Plus it'd take me half a day, whereas writing a SO question only takes me a few minutes :).

您可以说'Just test it'或'Just do a micro-benchmark',但测试多线程代码不是我的强项,我怀疑我能够模拟真实情况或准确假冒竞争条件。加上它需要我半天,而写一个问题只需要我几分钟:)。

3 个解决方案

#1


0  

You are reinventing Google-Collections/Guava's MapMaker/ComputingMap:

您正在重塑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);
}

Interning is not necessary here as the ComputingMap guarantees a single thread will only attempt to populate if absent and another thread asking for the same item will block and wait for the result. If you remove a key that is in the process of being populated then that thread and any that are currently waiting would still get that result but subsequent requests will start the population again.

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

If you do need interning, that library provides the excellent Interner class that has both strongly and weakly referenced caching.

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

#2


0  

synchronized takes up to 2 micro-seconds. Unless you need to cut this further you may be better off with the simplest solution.

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

BTW You can write

BTW你可以写

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

#3


0  

If "cache" is a map (which I suspect it is), then this problem is quite different than a simple double-checked locking problem.

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

If cache is a plain HashMap, then the problem is actually much worse; i.e. your proposed "double-checked pattern" behaves much worse than a simple reference-based double-checking. In fact, it can lead to ConcurrentModificationExceptions, getting incorrect values, or even an infinite loop.

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

If it is based on a plain HashMap, I would suggest using a ConcurrentHashMap as the first approach. With a ConcurrentHashMap, there is no explicit locking needed on your part.

如果它基于普通的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;
}

#1


0  

You are reinventing Google-Collections/Guava's MapMaker/ComputingMap:

您正在重塑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);
}

Interning is not necessary here as the ComputingMap guarantees a single thread will only attempt to populate if absent and another thread asking for the same item will block and wait for the result. If you remove a key that is in the process of being populated then that thread and any that are currently waiting would still get that result but subsequent requests will start the population again.

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

If you do need interning, that library provides the excellent Interner class that has both strongly and weakly referenced caching.

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

#2


0  

synchronized takes up to 2 micro-seconds. Unless you need to cut this further you may be better off with the simplest solution.

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

BTW You can write

BTW你可以写

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

#3


0  

If "cache" is a map (which I suspect it is), then this problem is quite different than a simple double-checked locking problem.

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

If cache is a plain HashMap, then the problem is actually much worse; i.e. your proposed "double-checked pattern" behaves much worse than a simple reference-based double-checking. In fact, it can lead to ConcurrentModificationExceptions, getting incorrect values, or even an infinite loop.

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

If it is based on a plain HashMap, I would suggest using a ConcurrentHashMap as the first approach. With a ConcurrentHashMap, there is no explicit locking needed on your part.

如果它基于普通的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;
}