我有许多单例实现的通用接口。接口定义了可以抛出已检查异常的初始化方法。
我需要一个工厂,它会根据需要返回缓存的单例实现,并想知道以下方法是否是线程安全的?
UPDATE1:请不要建议任何第3部分图书馆,因为这可能需要获得法律许可,因为可能的许可问题: - )
UPDATE2:此代码可能会在EJB环境中使用,因此最好不要生成其他线程或使用类似的东西。
interface Singleton
{
void init() throws SingletonException;
}
public class SingletonFactory
{
private static ConcurrentMap<String, AtomicReference<? extends Singleton>> CACHE =
new ConcurrentHashMap<String, AtomicReference<? extends Singleton>>();
public static <T extends Singleton> T getSingletonInstance(Class<T> clazz)
throws SingletonException
{
String key = clazz.getName();
if (CACHE.containsKey(key))
{
return readEventually(key);
}
AtomicReference<T> ref = new AtomicReference<T>(null);
if (CACHE.putIfAbsent(key, ref) == null)
{
try
{
T instance = clazz.newInstance();
instance.init();
ref.set(instance); // ----- (1) -----
return instance;
}
catch (Exception e)
{
throw new SingletonException(e);
}
}
return readEventually(key);
}
@SuppressWarnings("unchecked")
private static <T extends Singleton> T readEventually(String key)
{
T instance = null;
AtomicReference<T> ref = (AtomicReference<T>) CACHE.get(key);
do
{
instance = ref.get(); // ----- (2) -----
}
while (instance == null);
return instance;
}
}
我不完全确定第(1)和(2)行。我知道引用的对象在AtomicReference
中被声明为volatile字段,因此第(1)行所做的更改应该在第(2)行立即可见 - 但仍然有一些疑问......
除此之外 - 我认为使用ConcurrentHashMap
解决了将新密钥放入缓存的原子性。
你们看到这种方法有什么问题吗?谢谢!
PS:我知道静态持有者类习惯用法 - 由于ExceptionInInitializerError
(在单例实例化过程中抛出的任何异常被包装进入)和后续{{}我都不使用它1}}这不是我想要抓住的东西。相反,我想通过捕获它并优雅地处理它来利用专用检查异常的优势,而不是解析EIIR或NCDFE的堆栈跟踪。
答案 0 :(得分:3)
拥有所有这些并发/原子事件会导致更多的锁定问题,而不仅仅是
synchronized(clazz){}
围绕吸气剂阻挡。原子引用用于更新的引用,并且您不希望发生冲突。在这里你有一个作家,所以你不关心。
您可以通过使用散列图进一步优化它,并且只有在遗漏时才使用synchronized块:
public static <T> T get(Class<T> cls){
// No lock try
T ref = cache.get(cls);
if(ref != null){
return ref;
}
// Miss, so use create lock
synchronized(cls){ // singletons are double created
synchronized(cache){ // Prevent table rebuild/transfer contentions -- RARE
// Double check create if lock backed up
ref = cache.get(cls);
if(ref == null){
ref = cls.newInstance();
cache.put(cls,ref);
}
return ref;
}
}
}
答案 1 :(得分:3)
为了避免同步,你已经做了很多工作,我认为这样做的原因是出于性能问题。您是否测试了这是否确实提高了性能与同步解决方案的对比?
我问的原因是并发类往往比非并发类慢,更不用说使用原子引用的额外重定向级别了。根据您的线程争用,天真的同步解决方案实际上可能更快(并且更容易验证正确性)。
此外,我认为在调用instance.init()期间抛出SingletonException时,最终可能会出现无限循环。原因是在readEventually中等待的并发线程永远不会找到它的实例(因为在另一个线程正在初始化实例时抛出异常)。也许这是您的情况的正确行为,或者您可能想要为实例设置一些特殊值以触发将异常抛出到等待的线程。
答案 2 :(得分:0)
考虑使用Guava的CacheBuilder
。例如:
private static Cache<Class<? extends Singleton>, Singleton> singletons = CacheBuilder.newBuilder()
.build(
new CacheLoader<Class<? extends Singleton>, Singleton>() {
public Singleton load(Class<? extends Singleton> key) throws SingletonException {
try {
Singleton singleton = key.newInstance();
singleton.init();
return singleton;
}
catch (SingletonException se) {
throw se;
}
catch (Exception e) {
throw new SingletonException(e);
}
}
});
public static <T extends Singleton> T getSingletonInstance(Class<T> clazz) {
return (T)singletons.get(clazz);
}
注意:此示例未经测试且未编译。
Guava的底层Cache
实现将为您处理所有缓存和并发逻辑。
答案 3 :(得分:0)
这看起来会起作用,虽然我可能会考虑某种类型的睡眠,即使是纳秒或者在测试参考设置时也是如此。旋转测试循环将非常昂贵。
另外,我会考虑通过将AtomicReference
传递给readEventually()
来改进代码,这样您就可以避免containsKey()
然后putIfAbsent()
竞争条件。所以代码是:
AtomicReference<T> ref = (AtomicReference<T>) CACHE.get(key);
if (ref != null) {
return readEventually(ref);
}
AtomicReference<T> newRef = new AtomicReference<T>(null);
AtomicReference<T> oldRef = CACHE.putIfAbsent(key, newRef);
if (oldRef != null) {
return readEventually(oldRef);
}
...
答案 4 :(得分:-1)
代码通常不是线程安全的,因为CACHE.containsKey(key)
检查和CACHE.putIfAbsent(key, ref)
调用之间存在差距。两个线程可以同时调用该方法(特别是在多核/处理器系统上),并且都执行containsKey()
检查,然后两者都尝试执行put和creation操作。
我会使用锁定或在某种监视器上同步来保护getSingletonInstnace()
方法的执行。
答案 5 :(得分:-1)
google“Memoizer”。基本上,不是AtomicReference
,而是使用Future
。