这个Singleton是线程安全的吗?

4
基于这个话题,我提出了一个有趣的基于AtomicIntegers实现的Singleton模式版本。
问题如下:
  • 这个实现方式是正确的和线程安全的吗?一般来说,是否可以使用原子变量进行线程同步和管理呢?
  • 附加问题:如果这个实现是线程安全的,那么我真的需要为实例变量添加volatile修饰符吗?
public class StrangeSingleton
{

    private StrangeSingleton() {};

    private static volatile Object instance;

    private static AtomicInteger initCounter = new AtomicInteger();
    private static AtomicInteger readyCounter = new AtomicInteger();

    static Object getInstance()
    {

        if (initCounter.incrementAndGet() == 1)
        {
            instance = new Object();

            readyCounter.incrementAndGet();

            return instance;
        }
        else if (readyCounter.get() == 1)
        {
            return instance;
        }
        else
        {
            //initialization not complete yet.
            //write here some logic you want:               
            //sleep for 5s and try one more time,
            //or throw Exception, or return null..

            return null;
        }
    }
}

更新:添加了私有构造函数,但这不是重点。


2
你缺少了私有构造函数! - Santosh
当任何人都可以调用new StrangeSingleton()时,它怎么可能是单例?创建一个不带参数的私有构造函数。 - km1
2
你可以使用 AtomicBoolean 替代 AtomicInteger。使用 compareAndSet() 方法。 - Gray
1
@Santosh,你可以创建任意多个StrangeSingleton对象,但只会有一个instance实例对象。 - assylias
4个回答

8
这个实现是否正确且线程安全?通常可以使用原子变量来进行线程同步和管理吗?
这个实现是正确的,也是线程安全的。但是通常更复杂且需要占用更多的CPU资源,因为你需要忙等待以快速响应变化。
附加问题:如果此实现是线程安全的,我是否真的需要volatile修饰符来修饰实例变量?
在这种情况下不需要,因为AtomicInteger包含了volatile字段,可以确保正确的happens-before/happens-after行为。
当然,您也可以使用枚举类型,它是线程安全的且更简单 ;)
enum Singleton {
    INSTANCE;
}

Peter,从性能角度来看,这与通常的双重检查锁定机制相比如何? - Santosh
双重检查锁定比检查两个AtomicXxxxx更快。使用“枚举”是最快的,因为它不需要volatile字段和任何检查。 - Peter Lawrey
1
@Santosh 双重锁定在1.4及更早版本中也不一定在所有平台上都是安全的,因为存在一些实现语义问题。 ([维基百科文章。](http://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java))这在5+中不是问题。此外,+1。 - Brian
@Brian 我已经8年没有使用Java 1.4了,所以我忘记了这些事情。事实上,直到我开始使用Java 5.0之前,我都不知道这可能是个问题 ;) - Peter Lawrey
1
实例在第一次使用之前不会被加载。它与其他替代方案一样是惰性加载的。 - Peter Lawrey
显示剩余3条评论

2
这个实现是否正确且线程安全?一般来说,可以使用原子变量进行线程同步和管理吗?
是的,但对于readyCounter变量,您应该使用CountDownLatch,像这样:
private static AtomicInteger initCounter = new AtomicInteger();
private static CountDownLatch readyCounter = new CountDownLatch(1);

static Object getInstance()
{

    if (initCounter.incrementAndGet() == 1)
    {
        try {
            instance = new Object();
            return instance;
        } finally {
            readyCounter.countDown();
        }
    }
    else
    {
        readyCounter.await();
        return instance;
    }
}

调用 await() 方法也解决了初始化竞态条件。(我还添加了 try-finally 块以避免构造函数异常死锁。)

附加问题:如果此实现是线程安全的,那我是否真的需要为实例变量添加 volatile 修饰符?

不需要,只要在访问实例变量之前调用相关的 AtomicIntegerCountDownLatch 函数即可。请查看 文档 中的“happens-before”。

0

T1 线程可能会在 instance = new Object(); 处被挂起,因为 T2 还没有将 readyCounter 加 1,所以它将会进入 else{} 块。这并不完全正确,因为初始化已经完成,滞后的是状态记录。


0

我宁愿在你的getInstance()方法中做一个synchronized块。这已经足够了。你不需要那些奇怪的计数器,正如@David所指出的那样,它们也不是很安全。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接