java java中一个对象的线程安全缓存
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3636244/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Thread-safe cache of one object in java
提问by Igor Mukhin
let's say we have a CountryList object in our application that should return the list of countries. The loading of countries is a heavy operation, so the list should be cached.
假设我们的应用程序中有一个 CountryList 对象,它应该返回国家/地区列表。加载国家/地区是一项繁重的操作,因此应该缓存列表。
Additional requirements:
其他要求:
- CountryList should be thread-safe
- CountryList should load lazy (only on demand)
- CountryList should support the invalidation of the cache
- CountryList should be optimized considering that the cache will be invalidated very rarely
- CountryList 应该是线程安全的
- CountryList 应该延迟加载(仅按需加载)
- CountryList 应该支持缓存失效
- 考虑到缓存很少会失效,应该优化 CountryList
I came up with the following solution:
我想出了以下解决方案:
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);
}
}
What do you think about it? Do you see something bad about it? Is there other way to do it? How can i make it better? Should i look for totally another solution in this cases?
你怎么看待这件事?你看到它有什么不好的吗?还有其他方法吗?我怎样才能让它更好?在这种情况下,我应该完全寻找另一种解决方案吗?
Thanks.
谢谢。
采纳答案by Igor Mukhin
Thanks you all guys, especially to user "gid" who gave the idea.
谢谢大家,特别是提出这个想法的用户“ gid”。
My target was to optimize the performance for the get() operation considering the invalidate() operation will be called very rare.
我的目标是优化 get() 操作的性能,因为 invalidate() 操作将被称为非常罕见。
I wrote a testing class that starts 16 threads, each calling get()-Operation one million times. With this class I profiled some implementation on my 2-core maschine.
我编写了一个测试类,它启动 16 个线程,每个线程调用 get()-Operation 一百万次。通过这门课,我分析了我的 2 核机器上的一些实现。
Testing results
测试结果
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) "No synchronisation" is not thread-safe, but gives us the best performance that we can compare to.
1)“无同步”不是线程安全的,但为我们提供了可以比较的最佳性能。
@Override
public List<String> list() {
if (cache == null) {
cache = loadCountryList();
}
return cache;
}
@Override
public void invalidateCache() {
cache = null;
}
2) "Normal synchronisation" - pretty good performace, standard no-brainer implementation
2)“正常同步” - 非常好的性能,标准的简单实现
@Override
public synchronized List<String> list() {
if (cache == null) {
cache = loadCountryList();
}
return cache;
}
@Override
public synchronized void invalidateCache() {
cache = null;
}
3) "with MapMaker" - very poor performance.
3) “使用 MapMaker” - 性能很差。
See my question at the top for the code.
有关代码,请参阅我在顶部的问题。
4) "with Suppliers.memoize" - good performance. But as the performance the same "Normal synchronisation" we need to optimize it or just use the "Normal synchronisation".
4) “with Suppliers.memoize”——良好的表现。但是由于性能相同的“正常同步”,我们需要对其进行优化或仅使用“正常同步”。
See the answer of the user "gid" for code.
代码见用户“gid”的回答。
5) "with optimized memoize"- the performnce comparable to "no sync"-implementation, but thread-safe one. This is the one we need.
5)“具有优化的记忆”- 性能与“无同步”- 实现相当,但线程安全。这是我们需要的。
The cache-class itself: (The Supplier interfaces used here is from Google Collections Library and it has just one method get(). see http://google-collections.googlecode.com/svn/trunk/javadoc/com/google/common/base/Supplier.html)
缓存类本身:(此处使用的供应商接口来自 Google 集合库,它只有一种方法 get()。请参阅http://google-collections.googlecode.com/svn/trunk/javadoc/com/google/通用/基础/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;
}
}
}
Example use:
使用示例:
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");
}
}
回答by Gareth Davis
google collections actually supplies just the thing for just this sort of thing: Supplier
谷歌收藏实际上只为这种事情提供了东西:供应商
Your code would be something like:
您的代码将类似于:
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);
}
回答by Mike
Whenever I need to cache something, I like to use the Proxy pattern. Doing it with this pattern offers separation of concerns. Your original object can be concerned with lazy loading. Your proxy (or guardian) object can be responsible for validation of the cache.
每当我需要缓存某些东西时,我都喜欢使用Proxy 模式。使用这种模式来实现关注点分离。您的原始对象可能与延迟加载有关。您的代理(或监护人)对象可以负责缓存的验证。
In detail:
详细:
- Define an object CountryList class which is thread-safe, preferably using synchronization blocks or other semaphorelocks.
- Extract this class's interface into a CountryQueryable interface.
- Define another object, CountryListProxy, that implements the CountryQueryable.
- Only allow the CountryListProxy to be instantiated, and only allow it to be referenced through its interface.
- 定义一个线程安全的对象 CountryList 类,最好使用同步块或其他信号量锁。
- 将此类的接口提取到 CountryQueryable 接口中。
- 定义另一个对象 CountryListProxy,它实现 CountryQueryable。
- 只允许实例化CountryListProxy,只允许通过其接口引用。
From here, you can insert your cache invalidation strategy into the proxy object. Save the time of the last load, and upon the next request to see the data, compare the current time to the cache time. Define a tolerance level, where, if too much time has passed, the data is reloaded.
从这里,您可以将缓存失效策略插入到代理对象中。保存上次加载的时间,并在下一次请求查看数据时,将当前时间与缓存时间进行比较。定义容差级别,如果时间过长,则重新加载数据。
As far as Lazy Load, refer here.
至于延迟加载,请参考这里。
Now for some good down-home sample code:
现在来看一些不错的家庭示例代码:
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.
}
}
回答by Jay
I'm not sure what the map is for. When I need a lazy, cached object, I usually do it like this:
我不确定地图是做什么用的。当我需要一个懒惰的缓存对象时,我通常这样做:
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;
}
}
I think this is similar to what you're doing but a little simpler. If you have a need for the map and the ONE that you've simplified away for the question, okay.
我认为这与您正在做的类似,但更简单一些。如果您需要地图和已为问题简化的 ONE,那好吧。
If you want it thread-safe, you should synchronize the get and the forget.
如果您希望它是线程安全的,您应该同步获取和忘记。
回答by Bert F
What do you think about it? Do you see something bad about it?
你怎么看待这件事?你看到它有什么不好的吗?
Bleah - you are using a complex data structure, MapMaker, with several features (map access, concurrency-friendly access, deferred construction of values, etc) because of a single feature you are after (deferred creation of a single construction-expensive object).
Bleah - 您正在使用复杂的数据结构 MapMaker,它具有多个功能(地图访问、并发友好访问、值的延迟构造等),因为您追求的是单个功能(延迟创建单个构造昂贵的对象) .
While reusing code is a good goal, this approach adds additional overhead and complexity. In addition, it misleads future maintainers when they see a map data structure there into thinking that there's a map of keys/values in there when there is really only 1 thing (list of countries). Simplicity, readability, and clarity are key to future maintainability.
虽然重用代码是一个很好的目标,但这种方法会增加额外的开销和复杂性。此外,当未来的维护者看到地图数据结构时,它会误导未来的维护者认为那里有一个键/值的映射,而实际上只有一个东西(国家/地区列表)。简单性、可读性和清晰度是未来可维护性的关键。
Is there other way to do it? How can i make it better? Should i look for totally another solution in this cases?
还有其他方法吗?我怎样才能让它更好?在这种情况下,我应该完全寻找另一种解决方案吗?
Seems like you are after lazy-loading. Look at solutions to other SO lazy-loading questions. For example, this one covers the classic double-check approach (make sure you are using Java 1.5 or later):
似乎您在进行延迟加载。查看其他 SO 延迟加载问题的解决方案。例如,这个涵盖了经典的双重检查方法(确保您使用的是 Java 1.5 或更高版本):
How to solve the "Double-Checked Locking is Broken" Declaration in Java?
Rather than just simply repeat the solution code here, I think it is useful to read the discussion about lazy loading via double-check there to grow your knowledge base. (sorry if that comes off as pompous - just trying teach to fish rather than feed blah blah blah ...)
与其简单地重复这里的解决方案代码,我认为通过仔细检查那里阅读有关延迟加载的讨论来增加您的知识库很有用。(对不起,如果那显得浮夸——只是想教钓鱼而不是喂等等等等……)
回答by Chii
There is a library out there (from atlassian) - one of the util classes called LazyReference. LazyReference is a reference to an object that can be lazily created (on first get). it is guarenteed thread safe, and the init is also guarenteed to only occur once - if two threads calls get() at the same time, one thread will compute, the other thread will block wait.
那里有一个库(来自atlassian) - 称为LazyReference 的实用程序类之一。LazyReference 是对可以延迟创建的对象的引用(在第一次获取时)。它保证线程安全,并且 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();
回答by wolfcastle
Your needs seem pretty simple here. The use of MapMaker makes the implementation more complicated than it has to be. The whole double-checked locking idiom is tricky to get right, and only works on 1.5+. And to be honest, it's breaking one of the most important rules of programming:
您的需求在这里看起来很简单。MapMaker 的使用使实现变得比它必须的更复杂。整个双重检查锁定习惯用法很难正确使用,并且仅适用于 1.5+。老实说,它违反了最重要的编程规则之一:
Premature optimization is the root of all evil.
过早的优化是万恶之源。
The double-checked locking idiom tries to avoid the cost of synchronization in the case where the cache is already loaded. But is that overhead really causing problems? Is it worth the cost of more complex code? I say assume it is not until profiling tells you otherwise.
双重检查锁定习惯用法试图避免在缓存已加载的情况下的同步成本。但这种开销真的会导致问题吗?是否值得为更复杂的代码付出代价?我说假设不是直到分析告诉你否则。
Here's a very simple solution that requires no 3rd party code (ignoring the JCIP annotation). It does make the assumption that an empty list means the cache hasn't been loaded yet. It also prevents the contents of the country list from escaping to client code that could potentially modify the returned list. If this is not a concern for you, you could remove the call to Collections.unmodifiedList().
这是一个非常简单的解决方案,不需要第 3 方代码(忽略 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();
}
}
}
回答by romacafe
Follow up to Mike's solution above. My comment didn't format as expected... :(
跟进上面迈克的解决方案。我的评论没有按预期格式... :(
Watch out for synchronization issues in operationB, especially since load() is slow:
注意 operationB 中的同步问题,特别是因为 load() 很慢:
public String operationB() {
if (!loaded) {
load();
loaded = true;
}
//Do whatever.
return whatever;
}
You could fix it this way:
你可以这样修复它:
public String operationB() {
synchronized(loaded) {
if (!loaded) {
load();
loaded = true;
}
}
//Do whatever.
return whatever;
}
Make sure you ALWAYS synchronize on every access to the loaded variable.
确保在每次访问加载的变量时始终同步。
回答by helpermethod
Use the 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;
}
...
}
回答by stolsvik
This is way to simple to use the ComputingMap stuff. You only need a dead simple implementation where all methods are synchronized, and you should be fine. This will obviously block the first thread hitting it (getting it), and any other thread hitting it while the first thread loads the cache (and the same again if anyone calls the invalidateCache thing - where you also should decide whether the invalidateCache should load the cache anew, or just null it out, letting the first attempt at getting it again block), but then all threads should go through nicely.
这是使用 ComputingMap 东西的简单方法。你只需要一个简单的实现,所有方法都是同步的,你应该没问题。这显然会阻止第一个线程击中它(获取它),以及在第一个线程加载缓存时击中它的任何其他线程(如果有人调用 invalidateCache 东西,同样如此 - 您还应该决定 invalidateCache 是否应该加载重新缓存,或者只是将其清空,让第一次尝试再次获取它阻塞),但是所有线程都应该很好地通过。