假设我有以下内容(假设仅限于 java 1.4,因此没有泛型):
public class CacheManager {
static HashMap states;
static boolean statesLoaded;
public static String getState(String abbrev) {
if(!statesLoaded) {
loadStates();
}
return (String) states.get(abbrev);
}
private static void loadStates() {
//JDBC stuff to load the data
statesLoaded = true;
}
}
在 Web 应用服务器等高负载多线程环境中,如果超过 1 个线程尝试同时获取和加载缓存,理论上可能会出现问题。 (进一步假设网络应用程序上没有启动代码来初始化缓存)
仅仅使用 Collections.synchronizedMap 就足以解决这个问题吗?如果有很多线程访问返回的synchronizedMap,那么在执行 get() 时是否存在性能问题?
或者使用非同步的 HashMap 并在加载方法或布尔变量上进行同步会更好吗?我认为如果您同步其中任何一个,您可能最终会锁定该类。
例如,如果 load 方法是同步的,那么如果有 2 个线程同时进入 getStates() 方法,并且都看到 statesLoaded 为 false 会怎样。第一个获取方法上的锁,加载缓存并将 statesLoaded 设置为 true。不幸的是,第二个线程已经评估出 statesLoaded 为 false,并在锁释放后继续执行 load 方法。不会再次加载缓存吗?
在这种情况下加载缓存的最佳方法是利用 JVM 静态初始化:
public class CacheManager {
private static final HashMap states = new HashMap();
public static String getState(String abbrev) {
return (String) states.get(abbrev);
}
static {
//JDBC stuff to load the data
}
}
缓存将在第一次使用类时加载,并且由于静态初始化是线程安全的,因此映射将被安全地填充。任何后续调用来检索值都可以在不涉及任何锁定的情况下完成。
尽可能利用静态初始化总是一个好主意。它安全、高效,而且通常非常简单。
您应该同步此检查:
if(!statesLoaded) {
loadStates();
}
为什么?多个线程可以在地图上
get()
没有任何问题。但是,您需要原子地检查statesLoaded
标志,加载状态,设置标志,检查它。否则,您可以(比如说)加载状态,但该标志仍然不会被设置并且从另一个线程中可见。
(您可能会使其不同步,并允许多个线程重新初始化缓存,但至少这不是良好的编程实践,最坏的情况可能会导致您进一步遇到大缓存、不同实现等问题.)
因此,拥有同步地图是不够的(顺便说一句,这是一个很常见的误解)。
我不会担心同步对性能的影响。这在过去曾经是一个问题,但现在是一个更加轻量级的操作。与往常一样,在必要时进行测量和优化。过早的优化往往是浪费精力。
单例模式有什么问题?
public class CacheManager {
private static class SingletonHolder
{
static final HashMap states;
static
{
states = new HashMap();
states.put("x", "y");
}
}
public static String getState(String abbrev) {
return (String) SingletonHolder.states.get(abbrev);
}
}
由于 statesLoaded 只能从 false 变为 true,所以我会选择一个解决方案,首先检查 statesLoaded 是否为 true,如果是,则跳过初始化逻辑。如果不是,您锁定并再次检查,如果仍然为 false,则加载它的状态并将标志设置为 true。
这意味着在缓存初始化后调用 getState 的任何线程都将“提前退出”并在不加锁的情况下使用映射。
类似:
// If we safely know the states are loaded, don't even try to lock
if(!statesLoaded) {
// I don't even pretend I know javas synchronized syntax :)
lock(mutex);
// This second check makes sure we don't initialize the
// cache multiple times since it might have changed
// while we were waiting for the mutex
if(!statesLoaded) {
initializeStates();
statesLoaded = true;
}
release(mutex);
}
// Now you should know that the states are loaded and they were only
// loaded once.
这意味着锁定只会在实际初始化发生之前和期间涉及。
如果这是 C,我还要确保使
statesLoaded
variable
变得易失,以确保编译器优化第二次检查。我不知道 java 在这种情况下的行为如何,但我猜它会认为所有共享数据(例如 statesLoaded)在进入同步范围时可能是脏的。
不要尝试自己这样做。使用 Spring 或 Guice 等 IoC 容器并获取框架来为您管理和初始化单例。这使您的同步问题更容易管理。
+1 用于 IoC 容器。使用弹簧。创建 CacheManager 类作为非静态类,并在 Spring 上下文配置中定义 CacheManaget。
1 非静态CacheManager版本
package your.package.CacheManager;
// If you like annotation
@Component
public class CacheManager<K, V> {
private Map<K, V> cache;
public V get(K key) {
if(cache != null) {
return cache.get(key);
}
synchronized(cache) {
if(cache == null) {
loadCache();
}
return cache.get(key);
}
}
private void loadCache() {
cache = new HashMap<K, V>();
// Load from JDBC or what ever you want to load
}
}
2 在 Spring 上下文中定义 CacheManager 的 bean 或使用 @Service/@Component 注解(不要忘记为注解定义扫描路径)
<bean id="cacheManager" class="your.package.CacheManager"/>
3 使用Spring配置或@Autowire注释注入你想要的缓存bean
<bean id="cacheClient" clas="...">
<property name="cache" ref="cacheManager"/>
</bean>