【问题标题】:Thread-safe cache of one object in javajava中一个对象的线程安全缓存
【发布时间】:2011-04-07 20:47:32
【问题描述】:

假设我们的应用程序中有一个 CountryList 对象,它应该返回国家列表。国家的加载是一个繁重的操作,所以列表应该被缓存。

附加要求:

  • CountryList 应该是线程安全的
  • CountryList 应延迟加载(仅按需加载)
  • CountryList 应该支持缓存失效
  • CountryList 应该优化,因为缓存很少会失效

我想出了以下解决方案:

public class CountryList {
    private static final Object ONE = new Integer(1);

    // MapMaker is from Google Collections Library    
    private Map<Object, List<String>> cache = new MapMaker()
        .initialCapacity(1)
        .makeComputingMap(
            new Function<Object, List<String>>() {
                @Override
                public List<String> apply(Object from) {
                    return loadCountryList();
                }
            });

    private List<String> loadCountryList() {
        // HEAVY OPERATION TO LOAD DATA
    }

    public List<String> list() {
        return cache.get(ONE);
    }

    public void invalidateCache() {
        cache.remove(ONE);
    }
}

你怎么看?你看到它有什么不好的地方吗?还有其他方法吗?我怎样才能让它变得更好?在这种情况下,我应该寻找另一种解决方案吗?

谢谢。

【问题讨论】:

  • 我不相信这是线程安全的。如果两个线程同时调用 invalidateCache() 或者一个线程同时调用 list() 另一个线程调用 invalidateCache() 怎么办?
  • MapMaker 返回 Map 接口的线程安全实现。 makeComputingMap() 以原子方式进行计算 (google-collections.googlecode.com/svn/trunk/javadoc/com/google/…)
  • 您要接受答案吗?
  • @Gareth Davis,我已经接受了我自己的答案。
  • Integer.valueOf(1) 获取密钥的缓存版本。

标签: java caching lazy-loading


【解决方案1】:

谷歌收藏实际上只是为这类东西提供了东西:Supplier

您的代码将类似于:

private Supplier<List<String>> supplier = new Supplier<List<String>>(){
    public List<String> get(){
        return loadCountryList();
    }
};


// volatile reference so that changes are published correctly see invalidate()
private volatile Supplier<List<String>> memorized = Suppliers.memoize(supplier);


public List<String> list(){
    return memorized.get();
}

public void invalidate(){
    memorized = Suppliers.memoize(supplier);
}

【讨论】:

  • 不错的一个。谢谢。我刚刚测试了它。这确实提高了性能。
  • 什么是“违规”参考 :-)?
【解决方案2】:

谢谢大家,尤其是提出这个想法的用户“gid”。

我的目标是优化 get() 操作的性能,考虑到 invalidate() 操作将被称为非常罕见。

我写了一个测试类,它启动了 16 个线程,每个线程调用 get()-Operation 一百万次。通过这个课程,我在我的 2 核机器上分析了一些实现。

测试结果

Implementation              Time
no synchronisation          0,6 sec
normal synchronisation      7,5 sec
with MapMaker               26,3 sec
with Suppliers.memoize      8,2 sec
with optimized memoize      1,5 sec

1) “无同步”不是线程安全的,但可以提供我们可以比较的最佳性能。

@Override
public List<String> list() {
    if (cache == null) {
        cache = loadCountryList();
    }
    return cache;
}

@Override
public void invalidateCache() {
    cache = null;
}

2) “正常同步” - 相当不错的性能,标准的简单实现

@Override
public synchronized List<String> list() {
    if (cache == null) {
        cache = loadCountryList();
    }
    return cache;
}

@Override
public synchronized void invalidateCache() {
    cache = null;
}

3) “使用 MapMaker” - 性能非常差。

代码见顶部我的问题。

4) "with Suppliers.memoize" - 良好的表现。但是由于性能相同的“正常同步”我们需要对其进行优化或直接使用“正常同步”。

代码见用户“gid”的回答。

5) “优化记忆” - 性能与“无同步”实现相当,但线程安全。这是我们需要的。

缓存类本身: (此处使用的 Supplier 接口来自 Google Collections Library,它只有一个方法 get()。参见 http://google-collections.googlecode.com/svn/trunk/javadoc/com/google/common/base/Supplier.html

public class LazyCache<T> implements Supplier<T> {
    private final Supplier<T> supplier;

    private volatile Supplier<T> cache;

    public LazyCache(Supplier<T> supplier) {
        this.supplier = supplier;
        reset();
    }

    private void reset() {
        cache = new MemoizingSupplier<T>(supplier);
    }

    @Override
    public T get() {
        return cache.get();
    }

    public void invalidate() {
        reset();
    }

    private static class MemoizingSupplier<T> implements Supplier<T> {
        final Supplier<T> delegate;
        volatile T value;

        MemoizingSupplier(Supplier<T> delegate) {
            this.delegate = delegate;
        }

        @Override
        public T get() {
            if (value == null) {
                synchronized (this) {
                    if (value == null) {
                        value = delegate.get();
                    }
                }
            }
            return value;
        }
    }
}

使用示例:

public class BetterMemoizeCountryList implements ICountryList {

    LazyCache<List<String>> cache = new LazyCache<List<String>>(new Supplier<List<String>>(){
        @Override
        public List<String> get() {
            return loadCountryList();
        }
    });

    @Override
    public List<String> list(){
        return cache.get();
    }

    @Override
    public void invalidateCache(){
        cache.invalidate();
    }

    private List<String> loadCountryList() {
        // this should normally load a full list from the database,
        // but just for this instance we mock it with:
        return Arrays.asList("Germany", "Russia", "China");
    }
}

【讨论】:

  • 你能解释一下为什么优化版本更快吗?还有,你用这个例子试过了吗stackoverflow.com/a/3637441/638670这个版本和你的同步版本的区别在于你同步了整个方法,而你只需要同步访问缓存对象。
【解决方案3】:

每当我需要缓存某些东西时,我喜欢使用Proxy pattern。 使用这种模式可以提供关注点分离。你的原创 对象可以关注延迟加载。您的代理(或监护人)对象 可以负责缓存的验证。

详细说明:

  • 定义一个线程安全的对象 CountryList 类,最好使用同步块或其他semaphore 锁。
  • 将此类的接口提取到 CountryQueryable 接口中。
  • 定义另一个实现 CountryQueryable 的对象 CountryListProxy。
  • 只允许 CountryListProxy 被实例化,并且只允许它被引用 通过其界面。

从这里,您可以将缓存失效策略插入代理对象。保存上次加载的时间,下次请求查看数据时,将当前时间与缓存时间进行比较。定义一个容差水平,如果时间过长,则重新加载数据。

关于延迟加载,请参考here

现在来看一些不错的实用示例代码:

public interface CountryQueryable {

    public void operationA();
    public String operationB();

}

public class CountryList implements CountryQueryable {

    private boolean loaded;

    public CountryList() {
        loaded = false;
    }

    //This particular operation might be able to function without
    //the extra loading.
    @Override
    public void operationA() {
        //Do whatever.
    }

    //This operation may need to load the extra stuff.
    @Override
    public String operationB() {
        if (!loaded) {
            load();
            loaded = true;
        }

        //Do whatever.
        return whatever;
    }

    private void load() {
        //Do the loading of the Lazy load here.
    }

}

public class CountryListProxy implements CountryQueryable {

    //In accordance with the Proxy pattern, we hide the target
    //instance inside of our Proxy instance.
    private CountryQueryable actualList;
    //Keep track of the lazy time we cached.
    private long lastCached;

    //Define a tolerance time, 2000 milliseconds, before refreshing
    //the cache.
    private static final long TOLERANCE = 2000L;

    public CountryListProxy() {
            //You might even retrieve this object from a Registry.
        actualList = new CountryList();
        //Initialize it to something stupid.
        lastCached = Long.MIN_VALUE;
    }

    @Override
    public synchronized void operationA() {
        if ((System.getCurrentTimeMillis() - lastCached) > TOLERANCE) {
            //Refresh the cache.
                    lastCached = System.getCurrentTimeMillis();
        } else {
            //Cache is okay.
        }
    }

    @Override
    public synchronized String operationB() {
        if ((System.getCurrentTimeMillis() - lastCached) > TOLERANCE) {
            //Refresh the cache.
                    lastCached = System.getCurrentTimeMillis();
        } else {
            //Cache is okay.
        }

        return whatever;
    }

}

public class Client {

    public static void main(String[] args) {
        CountryQueryable queryable = new CountryListProxy();
        //Do your thing.
    }

}

【讨论】:

    【解决方案4】:

    您的需求在这里看起来很简单。 MapMaker 的使用使实现变得比它必须的更复杂。整个双重检查锁定习惯用法很难正确处理,并且仅适用于 1.5+。老实说,它违反了最重要的编程规则之一:

    过早的优化是 万恶之源。

    在缓存已经加载的情况下,双重检查锁定惯用语试图避免同步成本。但这种开销真的会导致问题吗?是否值得花费更复杂的代码?我说假设直到分析告诉你否则。

    这是一个非常简单的解决方案,不需要第三方代码(忽略 JCIP 注释)。它确实假设空列表意味着尚未加载缓存。它还可以防止国家/地区列表的内容转义到可能修改返回列表的客户端代码。如果这不是您关心的问题,您可以删除对 Collections.unmodifiedList() 的调用。

    public class CountryList {
    
        @GuardedBy("cache")
        private final List<String> cache = new ArrayList<String>();
    
        private List<String> loadCountryList() {
            // HEAVY OPERATION TO LOAD DATA
        }
    
        public List<String> list() {
            synchronized (cache) {
                if( cache.isEmpty() ) {
                    cache.addAll(loadCountryList());
                }
                return Collections.unmodifiableList(cache);
            }
        }
    
        public void invalidateCache() {
            synchronized (cache) {
                cache.clear();
            }
        }
    
    }
    

    【讨论】:

      【解决方案5】:

      我不确定地图的用途。当我需要一个惰性缓存对象时,我通常会这样做:

      public class CountryList
      {
        private static List<Country> countryList;
      
        public static synchronized List<Country> get()
        {
          if (countryList==null)
            countryList=load();
          return countryList;
        }
        private static List<Country> load()
        {
          ... whatever ...
        }
        public static synchronized void forget()
        {
          countryList=null;
        }
      }
      

      我认为这与您正在做的事情相似,但更简单一些。如果您需要地图和为问题简化的 ONE,可以。

      如果你希望它是线程安全的,你应该同步获取和忘记。

      【讨论】:

      • @iimuhin:因为它们只作用于静态数据。此类没有实例数据。只要一个函数可以是静态的,我就让它成为静态的。这稍微更有效率,可以作为读者的文档。
      【解决方案6】:

      你怎么看?你觉得它有什么不好的地方吗?

      Bleah - 您正在使用复杂的数据结构 MapMaker,它具有多个功能(地图访问、并发友好访问、值的延迟构造等),因为您追求的是单一功能(延迟创建单个构造-昂贵的物品)。

      虽然重用代码是一个很好的目标,但这种方法会增加额外的开销和复杂性。此外,当他们看到那里的地图数据结构时,它会误导未来的维护者认为那里有一个键/值的地图,而实际上只有一件事(国家列表)。简单性、可读性和清晰性是未来可维护性的关键。

      还有其他方法吗?我怎样才能让它变得更好?在这种情况下,我应该寻找另一种解决方案吗?

      好像你在延迟加载之后。查看其他 SO 延迟加载问题的解决方案。例如,这个涵盖了经典的双重检查方法(确保您使用的是 Java 1.5 或更高版本):

      How to solve the "Double-Checked Locking is Broken" Declaration in Java?

      与其只是简单地在这里重复解决方案代码,我认为通过仔细检查阅读有关延迟加载的讨论对增加您的知识库很有用。 (很抱歉,如果这看起来很自负——只是试图教鱼而不是喂鱼……)

      【讨论】:

        【解决方案7】:

        那里有一个库(来自atlassian)- 一个名为LazyReference 的实用程序类。 LazyReference 是对可以延迟创建的对象的引用(在第一次获取时)。它保证线程安全,并且 init 也保证只发生一次 - 如果两个线程同时调用 get(),一个线程将计算,另一个线程将阻塞等待。

        see a sample code:

        final LazyReference<MyObject> ref = new LazyReference() {
            protected MyObject create() throws Exception {
                // Do some useful object construction here
                return new MyObject();
            }
        };
        
        //thread1
        MyObject myObject = ref.get();
        //thread2
        MyObject myObject = ref.get();
        

        【讨论】:

        • 不错的提示,但此类不支持受控缓存失效。
        【解决方案8】:

        这对我来说看起来不错(我假设 MapMaker 来自 google 收藏?)理想情况下,您不需要使用 Map,因为您实际上没有键,但由于实现对我看不到的任何调用者隐藏这很重要。

        【讨论】:

          【解决方案9】:

          这是使用 ComputingMap 的简单方法。您只需要一个简单的实现,其中所有方法都同步,您应该没问题。这显然会阻止第一个线程命中它(获取它),以及在第一个线程加载缓存时命中它的任何其他线程(如果有人调用 invalidateCache 事物,则同样如此 - 您还应该决定 invalidateCache 是否应该加载重新缓存,或者只是将其清空,让第一次尝试再次阻止),但是所有线程都应该顺利通过。

          【讨论】:

            【解决方案10】:

            使用Initialization on demand holder idiom

            public class CountryList {
              private CountryList() {}
            
              private static class CountryListHolder {
                static final List<Country> INSTANCE = new List<Country>();
              }
            
              public static List<Country> getInstance() {
                return CountryListHolder.INSTANCE;
              }
            
              ...
            }
            

            【讨论】:

              【解决方案11】:

              跟进 Mike 的上述解决方案。我的评论没有按预期格式化... :(

              注意 operationB 中的同步问题,尤其是 load() 很慢:

              public String operationB() {
                  if (!loaded) {
                      load();
                      loaded = true;
                  }
              
                  //Do whatever.
                  return whatever;
              }
              

              你可以这样解决:

              public String operationB() {
                  synchronized(loaded) {
                      if (!loaded) {
                          load();
                          loaded = true;
                      }
                  }
              
                  //Do whatever.
                  return whatever;
              }
              

              确保在每次访问加载的变量时始终保持同步。

              【讨论】:

              • 您无法在原语上进行同步。您只能在对象上同步。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-07-11
              相关资源
              最近更新 更多