假设我们的应用程序中有一个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);
}
}
你怎么看?你觉得它有什么坏处吗?还有其他办法吗?我怎样才能让它变得更好?在这种情况下我应该寻找另一种解决方案吗?
感谢。
答案 0 :(得分:33)
您的代码类似于:
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);
}
答案 1 :(得分:17)
感谢所有人,特别是对于提出这个想法的用户“ gid ”。
我的目标是优化get()操作的性能,因为invalidate()操作将被称为非常罕见。
我编写了一个测试类,它启动了16个线程,每个线程调用get() - 操作一百万次。通过这个课程,我在我的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)“优化memoize” - 性能与“无同步”实现相当,但是线程安全。这是我们需要的。
缓存类本身: (此处使用的供应商界面来自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");
}
}
答案 2 :(得分:5)
每当我需要缓存某些内容时,我都喜欢使用Proxy pattern。 使用这种模式可以解决问题。你原来的 对象可以关注延迟加载。您的代理(或监护人)对象 可以负责验证缓存。
详细说明:
从此处,您可以将缓存失效策略插入代理对象。保存上次加载的时间,并在下次查看数据的请求时,将当前时间与缓存时间进行比较。定义容差级别,如果时间过长,则重新加载数据。
就延迟加载而言,请参阅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.
}
}
答案 3 :(得分:1)
我不确定地图的用途。当我需要一个懒惰的缓存对象时,我通常会这样做:
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;
}
}
我认为这与你正在做的相似,但有点简单。如果您需要地图以及您已针对该问题简化过的那个,那么。
如果你想要它是线程安全的,你应该同步get和forget。
答案 4 :(得分:1)
你怎么看?你觉得它有什么不好吗?
Bleah - 您正在使用复杂的数据结构MapMaker,它具有多个功能(地图访问,并发友好访问,值的延迟构造等),因为您正在使用的单个功能(延迟创建单个结构 - 昂贵的物品)。
虽然重用代码是一个很好的目标,但这种方法增加了额外的开销和复杂性。此外,当他们看到地图数据结构时会误导未来的维护者,以为当那里只有一件事(国家列表)时,会有一个键/值的映射。简单性,可读性和清晰度是未来可维护性的关键。
还有其他办法吗?我怎样才能让它变得更好?在这种情况下我应该寻找另一种解决方案吗?
好像你是在懒惰加载之后。看看其他SO延迟加载问题的解决方案。例如,这个涵盖了经典的双重检查方法(确保您使用的是Java 1.5或更高版本):
How to solve the "Double-Checked Locking is Broken" Declaration in Java?
这里不仅仅是简单地重复解决方案代码,我认为通过仔细检查有关延迟加载的讨论是有用的,以增加您的知识库。 (对不起,如果那是浮夸的 - 只是尝试教鱼而不是喂等等等等......)
答案 5 :(得分:1)
那里有一个库(来自atlassian) - 一个名为LazyReference的util类。 LazyReference是对可以延迟创建的对象的引用(在第一次获取时)。它是guarenteed线程安全的,并且init也被保证只发生一次 - 如果两个线程同时调用get(),一个线程将计算,另一个线程将阻塞等待。
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();
答案 6 :(得分:1)
这里的需求似乎很简单。 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();
}
}
}
答案 7 :(得分:0)
这对我来说没问题(我假设MapMaker来自google集合?)理想情况下你不需要使用Map,因为你没有真正的密钥,但是因为我没有看到任何调用者隐藏了实现这是一个大问题。
答案 8 :(得分:0)
这是使用ComputingMap的简单方法。你只需要一个简单的实现,所有方法都是同步的,你应该没问题。这显然会阻止第一个线程命中它(获取它),以及任何其他线程在第一个线程加载缓存时命中它(如果有人调用invalidateCache事件,则再次相同 - 你还应该决定invalidateCache是否应该加载重新缓存,或者只是将其清空,让第一次尝试再次阻止它),但是所有线程都应该很好地完成。
答案 9 :(得分:0)
使用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;
}
...
}
答案 10 :(得分:0)
跟进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;
}
确保在每次访问已加载的变量时始终保持同步。