加载静态缓存的最佳模式或方法是什么?

问题描述 投票:0回答:6

假设我有以下内容(假设仅限于 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 方法。不会再次加载缓存吗?

java static
6个回答
7
投票

在这种情况下加载缓存的最佳方法是利用 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
    }
}

缓存将在第一次使用类时加载,并且由于静态初始化是线程安全的,因此映射将被安全地填充。任何后续调用来检索值都可以在不涉及任何锁定的情况下完成。

尽可能利用静态初始化总是一个好主意。它安全、高效,而且通常非常简单。


1
投票

您应该同步此检查:

if(!statesLoaded) {
    loadStates();
}

为什么?多个线程可以在地图上

get()
没有任何问题。但是,您需要原子地检查
statesLoaded
标志,加载状态,设置标志,检查它。否则,您可以(比如说)加载状态,但该标志仍然不会被设置并且从另一个线程中可见。

(您可能会使其不同步,并允许多个线程重新初始化缓存,但至少这不是良好的编程实践,最坏的情况可能会导致您进一步遇到大缓存、不同实现等问题.)

因此,拥有同步地图是不够的(顺便说一句,这是一个很常见的误解)。

我不会担心同步对性能的影响。这在过去曾经是一个问题,但现在是一个更加轻量级的操作。与往常一样,在必要时进行测量和优化。过早的优化往往是浪费精力。


0
投票

单例模式有什么问题?

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

}

0
投票

由于 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)在进入同步范围时可能是脏的。


0
投票

不要尝试自己这样做。使用 Spring 或 Guice 等 IoC 容器并获取框架来为您管理和初始化单例。这使您的同步问题更容易管理。


-1
投票

+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>
© www.soinside.com 2019 - 2024. All rights reserved.