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

时间:2009-07-17 15:26:18

标签: java static

假设我有以下内容(假设仅限于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个线程尝试同时获取并加载缓存。 (进一步假设Web应用程序上没有用于初始化缓存的启动代码)

只是使用Collections.synchronizedMap足以解决这个问题吗?如果很多线程正在访问它,那么返回的synchronizedMap在执行get()时会出现性能问题吗?

或者更好的是有一个非同步的HashMap,而是在load方法或boolean变量上同步?我认为如果你同步其中任何一个,你可能会最终锁定该类。

例如,如果load方法已同步,那么如果两个线程同时进入getStates()方法,并且两者都看到statesLoaded为false。第一个获取方法的锁定,加载缓存并将statesLoaded设置为true。不幸的是,第二个线程已经评估了statesLoaded为false,并且一旦锁定空闲就进入load方法。它不会继续并再次加载缓存吗?

6 个答案:

答案 0 :(得分:6)

在这种情况下加载缓存的最佳方法是利用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 :(得分:1)

您应该同步此检查:

if(!statesLoaded) {
    loadStates();
}

为什么?地图上的多个线程get()可以没有任何问题。但是,您需要原子检查statesLoaded标志,加载状态并设置标志,检查它。否则你可以(比方说)加载状态,但是标志仍然没有被设置并且从另一个线程可见。

(你可能会让这个不同步并允许多个线程重新初始化缓存的可能性,但至少它不是很好的编程习惯,并且最坏的情况可能会导致你的问题进一步发生大缓存,不同实现等。)

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

我不担心同步对性能的影响。它曾经是一个过去的问题,但现在是一个更轻量级的操作。一如既往,在必要时进行测量和优化。过早优化往往是一种浪费的努力。

答案 2 :(得分:0)

请勿尝试自己动手。使用像Spring或Guide这样的IoC容器,获取框架来管理和初始化单例。这使您的同步问题更易于管理。

答案 3 :(得分:0)

Singleton模式有什么问题?

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

}

答案 4 :(得分:0)

由于statesLoaded只能从false变为true,所以我会寻求一个解决方案,首先检查statesLoaded是否为真,如果只是跳过初始化逻辑。如果不是你锁定并再次检查,如果它仍然是假的,你加载状态并将标志设置为真。

这意味着在初始化缓存之后调用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)在进入同步范围时可能会变脏。

答案 5 :(得分:0)

IoC容器的

+1。使用Spring。将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>