Java 单例对象工厂:这段代码是线程安全的吗?
我有一个用于许多单例实现的通用接口。接口定义了可以引发选中异常的初始化方法 我需要一个工厂,它将按需返回缓存的单例实现,并想知道下面的方法是否是线程安全的 更新1:请不要建议任何第三方图书馆,因为这需要获得法律许可,因为可能存在许可问题:-) UPDATE2:此代码可能会在EJB环境中使用,因此最好不要产生额外的线程或使用类似的东西Java 单例对象工厂:这段代码是线程安全的吗?,java,thread-safety,singleton,factory,atomic,Java,Thread Safety,Singleton,Factory,Atomic,我有一个用于许多单例实现的通用接口。接口定义了可以引发选中异常的初始化方法 我需要一个工厂,它将按需返回缓存的单例实现,并想知道下面的方法是否是线程安全的 更新1:请不要建议任何第三方图书馆,因为这需要获得法律许可,因为可能存在许可问题:-) UPDATE2:此代码可能会在EJB环境中使用,因此最好不要产生额外的线程或使用类似的东西 接口单例 { void init()抛出SingletonException; } 公营单音工厂 { 私有静态ConcurrentMap考虑使用番石榴。例如: pr
接口单例
{
void init()抛出SingletonException;
}
公营单音工厂
{
私有静态ConcurrentMap考虑使用番石榴。例如:
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);
}
private static Cache由于Cache.containsKey(key)
检查和Cache.putIfAbsent(key,ref)
调用之间存在间隙,因此代码通常不是线程安全的。两个线程可以同时调用方法(特别是在多核/处理器系统上)并都执行containsKey()
检查,然后都尝试执行put和creation操作
我将使用锁或在某种监视器上进行同步来保护getSingleTonistNACE()方法的执行。拥有所有这些并发/原子的东西将导致更多的锁问题,而不仅仅是将
synchronized(clazz){}
getter周围的块。原子引用用于更新的引用,您不希望发生冲突。在这里,您只有一个编写器,所以您不关心这一点
您可以通过使用hashmap对其进行进一步优化,并且只有在未命中时,才使用同步块:
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;
}
}
}
publicstatict-get(类cls){
//无锁尝试
T ref=cache.get(cls);
如果(ref!=null){
返回ref;
}
//小姐,请使用创建锁
已同步(cls){//单例是双重创建的
已同步(缓存){//防止表重建/传输冲突--很少
//再次检查是否备份了创建锁
ref=cache.get(cls);
如果(ref==null){
ref=cls.newInstance();
cache.put(cls,ref);
}
返回ref;
}
}
}
> > > P>这看起来是可行的,虽然我可能考虑某种睡眠,即使是纳秒,也可以是在测试时要设置的基准。自旋测试循环会非常昂贵。
也可以考虑通过将<代码> AtomicReference < /代码>改为<代码> ReaDeDeUnEuffor()/代码>,从而可以避免<代码>包含密钥()/代码>,然后>代码> PuthabBebug()/<代码>竞争条件。因此代码将是:
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);
}
...
AtomicReference=(AtomicReference)CACHE.get(key);
如果(ref!=null){
返回ref;
}
AtomicReference newRef=新的AtomicReference(null);
AtomicReference oldRef=CACHE.putIfAbsent(key,newRef);
如果(oldRef!=null){
返回READREF(oldRef);
}
...
为了避免同步,您已经做了很多工作,我认为这样做的原因是出于性能考虑。您是否测试过,与同步解决方案相比,这是否确实提高了性能
我询问的原因是并发类往往比非并发类慢,更不用说原子引用的额外重定向级别了。根据线程争用情况,简单的同步解决方案实际上可能更快(并且更容易验证正确性)
此外,我认为在调用instance.init()期间引发SingletonException时,可能会导致无限循环。原因是等待ReadFinally的并发线程永远不会找到其实例(因为在另一个线程初始化实例时引发了异常)。可能这是您案例的正确行为,或者您希望为实例设置一些特殊值,以触发异常,并将其抛出到等待的线程。google“Memoizer”.基本上,使用Future
代替AtomicReference
,他通过将AtomicReference
放入一个null
值来防止这种情况。如果putIfAbsent()
不返回null,它只是将其丢弃并调用get。这就是循环的要点。谢谢!我考虑过这种方法,但在“Java并发实践”中有一个推理书中提到,中等负载下基于原子的算法优于基于锁类的算法,而基于锁类的算法又优于基于内在锁的算法。然而,我倾向于你建议的代码,因为它更具可读性:-)是的,但作为一个单例,你永远不会达到负载,因为创建是唯一锁定的部分。获取程序都是不同步的根据我的经验,旋转总是比锁阻塞更糟糕。除了我之前的评论之外,我认为普通的java.util.HashMap在这里是不够的,因为cache.put(cls,ref)
可以触发内部表的重建,从而cache.get(cls)
可以看到处于非一致状态的映射,因为后一个调用是由synchronized
块生成的。好吧,我刚刚检查了源代码,您是正确的,如果在对两个GET进行访问的同时,一行两次调整和传输实体表的大小,可能会有一个非常遥远的机会。这是非常可笑的rem注意,为了防止双放入重建问题,可以添加同步(缓存)关于doublecheck/put块——我正在编辑我的答案来说明这一点。小心,即使有额外的同步,除非您在所有GET上同步,否则您仍然有可能与常规HashMap出现不一致的状态。此外,我相信即使您已经同步了写入,因为