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

时间:2022-12-10 00:39:44

Let's say I have the following (assume restricted to java 1.4 so no generics) :

假设我有以下内容(假设仅限于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;
    }
}

In a high-load multi-thread environment like a web app server, this could theoretically have problems if > 1 thread tries to get and load the cache at the same time. (Further assuming there's no startup code on the web app to initialize the cache)

在像Web应用程序服务器这样的高负载多线程环境中,如果> 1线程试图同时获取并加载缓存,理论上这可能会出现问题。 (进一步假设Web应用程序上没有用于初始化缓存的启动代码)

Is simply using Collections.synchronizedMap sufficient to fix this? Does the returned synchronizedMap have performance issues when doing get(), if a lot of threads are accessing it?

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

Or would it be better to have a non-synchronized HashMap, and instead synchronize on the load method or boolean variable? I would think that if you synchronized either of those, you might end up locking the class.

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

For instance, if the load method was synchronized, what if 2 threads enter the getStates() method at the same time, and both see that statesLoaded is false. The first one gets a lock on the method, loads the cache and sets statesLoaded to true. Unfortunately, the 2nd thread has already evaluated that statesLoaded was false, and proceeds to the load method once the lock is free. Won't it go ahead and load the cache again?

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

6 个解决方案

#1


The best way to load the cache in this case is to take advantage of the JVM static initialization:

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

The cache will be loaded the first time the class is being used, and since the static initialization is thread safe, the map will be populated safely. Any subsequent calls to retrieve the values can be done without any locking involved.

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

It is always a good idea to take advantage of static initialization whenever possible. It is safe, efficient, and often quite simple.

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

#2


You should synchronize this check:

您应该同步此检查:

if(!statesLoaded) {
    loadStates();
}

Why ? Multiple threads can get() on the map without any problems. However, you need to atomically check the statesLoaded flag, load the state, and set the flag, check it. Otherwise you could (say) load the states, but the flag would still not be set and be visible as such from another thread.

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

(You could potentially leave this unsynchronised and allow the possibility of multiple threads to re-initialise the cache, but at the least it's not good programming practise, and at worst could cause you problems further down the line with large caches, differing implementations etc.)

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

Consequently, having a synchronised map isn't enough (this is quite a common misunderstanding, btw).

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

I wouldn't worry about the performance impact of synchronisation. It used to be an issue in the past, but is a much more lightweight operation now. As always, measure and optimise when you have to. Premature optimisation is often a wasted effort.

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

#3


Don't try and do this yourself. Use an IoC container like Spring or Guide and get the framework to manage and initialise the singleton for you. This makes your synchronization problems much more manageable.

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

#4


whats wrong with the Singleton pattern?

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

}

#5


Since statesLoaded only can go from false to true I'd go for a solution where you first check the statesLoaded is true, if it is you just skip the initalization logic. If it isn't you lock and check again and if it's still false you load the states it and set the flag to true.

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

This means that any thread calling getState after the cache is initialized will "early out" and use the map without locking.

这意味着在初始化缓存之后调用getState的任何线程都将“提前”并使用映射而不锁定。

something like:

// 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.

This means that the locking will only be involved before and during the actual initalization happens.

这意味着只有在实际初始化发生之前和期间才会涉及锁定。

If this would be C, I'd also make sure to make the statesLoaded variable to be volatile to make sure the compiler optimize the second check. I don't know how java behaves when it comes to situations like that but I would guess it considers all shared data such as statesLoaded to be potentially dirty when going into synchronization scopes.

如果这是C,我还要确保使statesLoaded变量为volatile,以确保编译器优化第二次检查。我不知道java在这种情况下的表现如何,但我猜它会认为所有共享数据(例如statesLoaded)在进入同步范围时可能会变脏。

#6


+1 for IoC container. Use Spring. Create CacheManager class as non static one and define CacheManaget in Spring context config.

IoC容器+1。使用Spring。将CacheManager类创建为非静态类,并在Spring上下文配置中定义CacheManaget。

1 Non-static CacheManager version

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 Define bean of CacheManager in spring context or use @Service/@Component annotation (don't foget define scan path for annotations)

2在spring上下文中定义CacheManager的bean或使用@Service / @Component注释(不要为注释定义扫描路径)

<bean id="cacheManager" class="your.package.CacheManager"/>

3 Inject your cache bean what ever you want with Spring config or @Autowire annotation

3使用Spring配置或@Autowire注释注入您想要的缓存bean

<bean id="cacheClient" clas="...">
    <property name="cache" ref="cacheManager"/>
</bean>

#1


The best way to load the cache in this case is to take advantage of the JVM static initialization:

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

The cache will be loaded the first time the class is being used, and since the static initialization is thread safe, the map will be populated safely. Any subsequent calls to retrieve the values can be done without any locking involved.

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

It is always a good idea to take advantage of static initialization whenever possible. It is safe, efficient, and often quite simple.

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

#2


You should synchronize this check:

您应该同步此检查:

if(!statesLoaded) {
    loadStates();
}

Why ? Multiple threads can get() on the map without any problems. However, you need to atomically check the statesLoaded flag, load the state, and set the flag, check it. Otherwise you could (say) load the states, but the flag would still not be set and be visible as such from another thread.

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

(You could potentially leave this unsynchronised and allow the possibility of multiple threads to re-initialise the cache, but at the least it's not good programming practise, and at worst could cause you problems further down the line with large caches, differing implementations etc.)

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

Consequently, having a synchronised map isn't enough (this is quite a common misunderstanding, btw).

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

I wouldn't worry about the performance impact of synchronisation. It used to be an issue in the past, but is a much more lightweight operation now. As always, measure and optimise when you have to. Premature optimisation is often a wasted effort.

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

#3


Don't try and do this yourself. Use an IoC container like Spring or Guide and get the framework to manage and initialise the singleton for you. This makes your synchronization problems much more manageable.

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

#4


whats wrong with the Singleton pattern?

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

}

#5


Since statesLoaded only can go from false to true I'd go for a solution where you first check the statesLoaded is true, if it is you just skip the initalization logic. If it isn't you lock and check again and if it's still false you load the states it and set the flag to true.

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

This means that any thread calling getState after the cache is initialized will "early out" and use the map without locking.

这意味着在初始化缓存之后调用getState的任何线程都将“提前”并使用映射而不锁定。

something like:

// 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.

This means that the locking will only be involved before and during the actual initalization happens.

这意味着只有在实际初始化发生之前和期间才会涉及锁定。

If this would be C, I'd also make sure to make the statesLoaded variable to be volatile to make sure the compiler optimize the second check. I don't know how java behaves when it comes to situations like that but I would guess it considers all shared data such as statesLoaded to be potentially dirty when going into synchronization scopes.

如果这是C,我还要确保使statesLoaded变量为volatile,以确保编译器优化第二次检查。我不知道java在这种情况下的表现如何,但我猜它会认为所有共享数据(例如statesLoaded)在进入同步范围时可能会变脏。

#6


+1 for IoC container. Use Spring. Create CacheManager class as non static one and define CacheManaget in Spring context config.

IoC容器+1。使用Spring。将CacheManager类创建为非静态类,并在Spring上下文配置中定义CacheManaget。

1 Non-static CacheManager version

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 Define bean of CacheManager in spring context or use @Service/@Component annotation (don't foget define scan path for annotations)

2在spring上下文中定义CacheManager的bean或使用@Service / @Component注释(不要为注释定义扫描路径)

<bean id="cacheManager" class="your.package.CacheManager"/>

3 Inject your cache bean what ever you want with Spring config or @Autowire annotation

3使用Spring配置或@Autowire注释注入您想要的缓存bean

<bean id="cacheClient" clas="...">
    <property name="cache" ref="cacheManager"/>
</bean>