0

我记得在某处读过 android 保证 LruCache 为所有线程提供最新信息,并且一个线程的操作将在同一个线程看到另一个线程对缓存的编辑之前完成。我正在使用 LruCache 存储从我的应用程序服务器获取的位图,并使用线程池从网络获取位图。

现在我在 Android 文档或任何其他提及中找不到对此的引用。我是否需要将 LruCache 实例标记为 volatile 或围绕缓存操作设置同步(LruCache)?

4

1 回答 1

2

mibollma 关于Android LruCache Thread Safety的回应没有错。人们经常误认为线程安全和原子性。

如果一个类是线程安全的,这意味着,例如,当两个线程调用它的操作时,内部不会中断。Vector就是这样一个类,每个操作都是同步的。如果两个不同的线程调用 Vector.add,它们都会在实例上同步并且状态不会被破坏。例如这样的:

synchronized void add(final T obj) {
  objects[index++] = obj;
}

这种线程安全的意思是没有两个线程会在同一位置添加元素。如果它不会同步,他们可以读取 index = 0 并尝试在该位置写入。

现在为什么还需要同步?想象一下,你有这样一个案例:

if(!collection.contains(element)) {
  collection.add(element);
}

在这种情况下,您的操作不是原子的。当您询问元素是否已经存在时,您会同步一次,然后在添加该元素时进行第二次同步。但是当另一个线程可以取得进展并且您对不包含该元素的集合的假设被打破时,这两个调用之间存在一个窗口。

在伪代码中:

if(!coll.contains(element)) {         // << you have the exclusive lock here
  //Thread 2 calls coll.add(element)     << you do not have the lock anymore
  coll.add(element);                  // << doomed!
}

所以这就是为什么答案是正确的,因为您应该围绕非原子操作进行同步,例如

synchronized(coll) {
    if(!coll.contains(element)) {       // << you have the exclusive lock here
      // Thread 2 wants to call            << still holding the lock
      // coll.add(element) but
      // cannot because you hold the lock
      coll.add(element);                // << unicorns!
    }
}

因为同步非常昂贵,并发集合带有原子操作,例如putIfAbsent.

现在回到你最初的问题:你应该让 LruCache 易失吗?通常,您不会将 LruCache 本身标记为 volatile,而是将其标记为对它的引用。如果这样的引用是跨线程共享的,并且您计划更新该字段,那么可以。

如果字段未标记为 volatile,则线程可能看不到更新的值。但同样:这只是对 LruCache 本身的引用,与它的内容没有直接关系。

在您的特定场景中,我宁愿使用 final 而不是 volatile 引用,因为null无论如何都没有线程应该将引用设置为。

是否需要围绕缓存操作进行同步的问题取决于具体情况。如果您想创建单个原子操作,例如putIfAbsent,那么可以。

public void putIfAbsent(final K key, final V value) {
  synchronized(lruCache) {
    if(!lruCache.containsKey(key)) {
      lruCache.put(key, value);
    }
  }
}

但是稍后在您的代码中,当您调用 just 时lruCache.get(key),无需将其包装到同步块本身中。仅当您计划创建不应干扰另一个线程的原子操作时。

于 2013-04-15T13:48:21.553 回答