java中一个对象的线程安全缓存

时间:2010-09-03 13:33:49

标签: java caching lazy-loading

假设我们的应用程序中有一个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);
    }
}

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

感谢。

11 个答案:

答案 0 :(得分:33)

谷歌收藏品实际上只是提供这类东西: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);
}

答案 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。 使用这种模式可以解决问题。你原来的 对象可以关注延迟加载。您的代理(或监护人)对象 可以负责验证缓存。

详细说明:

  • 定义一个对象的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.
    }

}

答案 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(),一个线程将计算,另一个线程将阻塞等待。

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();

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

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