繁体   English   中英

单例对象的工厂:这个代码是线程安全的吗?

[英]Factory of singleton objects: is this code thread-safe?

我有一个用于许多单例实现的通用接口。 接口定义了可以抛出检查异常的初始化方法。

我需要一个工厂,它将按需返回缓存的单例实现,并想知道以下方法是否是线程安全的?

更新1:请不要建议任何第3部分图书馆,因为这可能需要获得法律许可,因为可能的许可问题:-)

更新2:此代码可能会在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 (在单例实例化过程中抛出的任何异常被包装)和随后的NoClassDefFoundError不是我想要捕获的东西。 相反,我想通过捕获它并优雅地处理它来利用专用检查异常的优势,而不是解析EIIR或NCDFE的堆栈跟踪。

拥有所有这些并发/原子事件会导致更多的锁定问题,而不仅仅是放置

synchronized(clazz){}

在吸气器周围阻挡。 原子引用用于更新的引用,并且您不希望发生冲突。 在这里你有一个作家,所以你不关心。

您可以通过使用hashmap进一步优化它,并且只有在未命中时才使用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;
        }
    }
}

你已经做了大量的工作来避免同步,我认为这样做的原因是出于性能问题。 您是否已经过测试,看看这实际上是否提高了性能与同步解决方案?

我问的原因是并发类往往比非并发类慢,更不用说使用原子引用的额外重定向级别。 根据您的线程争用,天真的同步解决方案实际上可能更快(并且更容易验证正确性)。

另外,我认为在调用instance.init()期间抛出SingletonException时,最终可能会出现无限循环。 原因是在readEventually中等待的并发线程永远不会找到它的实例(因为在另一个线程正在初始化实例时抛出异常)。 也许这是您的情况的正确行为,或者您可能希望为实例设置一些特殊值以触发将异常抛出到等待的线程。

考虑使用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实现将为您处理所有缓存和并发逻辑。

这看起来会起作用,虽然我可能会考虑某种类型的睡眠,即使是纳秒或测试参考设置的东西。 旋转测试循环将非常昂贵。

另外,我会考虑通过将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);
}
...

代码通常不是线程安全的,因为CACHE.containsKey(key)检查和CACHE.putIfAbsent(key, ref)调用之间存在间隙。 两个线程可以同时调用该方法(特别是在多核/处理器系统上)并且都执行containsKey()检查,然后两者都尝试执行put和creation操作。

我会使用锁或在某种监视器上同步来保护getSingletonInstnace()方法的执行。

谷歌“记事本”。 基本上,使用Future代替AtomicReference

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM