Oop 为什么我们要同步懒惰的单身汉而不是渴望的单身汉?

Oop 为什么我们要同步懒惰的单身汉而不是渴望的单身汉?,oop,synchronization,singleton,lazy-initialization,Oop,Synchronization,Singleton,Lazy Initialization,典型的懒惰单身汉: public class Singleton { private static Singleton INSTANCE; private Singleton() { } public static synchronized Singleton getInstace() { if(INSTANCE == null) INSTANCE = new Singleton(); return

典型的懒惰单身汉:

public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {

    }

    public static synchronized Singleton getInstace() {
        if(INSTANCE == null)
            INSTANCE = new Singleton();

        return INSTANCE;
    }
}
public class Singleton {
    private static Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }
}
典型的渴望单身:

public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {

    }

    public static synchronized Singleton getInstace() {
        if(INSTANCE == null)
            INSTANCE = new Singleton();

        return INSTANCE;
    }
}
public class Singleton {
    private static Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

为什么我们不关心与急切的单例的同步,而必须担心与它们懒惰的表亲的同步?

因为当类首次加载到内存(jit)中时,急切的单例是初始化的,并且这种情况只发生一次。但是,如果两个客户端试图同时从两个线程调用singleton实例方法,则可能会创建两个singleton。

因为在后一个示例中,调用
getInstance
时,singleton实例始终存在-此处无需同步。这与第一个示例相反,第一个示例不需要初始化实例。在这种情况下,
getInstance
包含需要保护(例如通过同步)的关键部分(if及其主体)反对同时访问。

急切实例化不需要显式同步来共享字段引用,因为JVM已经作为类加载机制的一部分为我们处理了它

更详细地说,在类可供任何线程使用之前,它将被加载、验证和初始化。编译器将静态字段赋值重写到此初始化阶段,并通过Java内存模型和底层硬件体系结构的规则确保访问该类的所有线程都将看到该类的此版本。这意味着JVM将为我们处理任何硬件障碍等

话虽如此,我还是建议大家在急切的草签仪式上做最后的标记。这将使您的意图更加清晰,编译器将强制执行渴望的初始化永不更改。如果这样做了,那么将再次需要并发控制

private static **final** Singleton INSTANCE = new Singleton();
仅供参考,如果您感兴趣,本手册第5.5节将更详细地介绍这一点。规范中的几个精选片段如下

*"Because the Java Virtual Machine is multithreaded, 
initialization of a class or interface requires careful 
synchronization"*

*"For each class or interface C, there is a unique initialization lock LC"*

*9&10) "Next, execute the class or interface initialization method of C"
"If the execution of the class or interface initialization 
method completes normally, then acquire LC, label the Class object for 
C as fully initialized, notify all waiting threads, release LC, and 
complete this procedure normally."*

在规范的第10步中,将设置静态字段,并使用锁(LC)来确保只有一个线程执行初始化,并且结果被正确共享。

因为没有可能的竞争条件?@OliCharlesworth-但是懒惰的单线程不会竞争,他们只是慢慢地走。谢谢@OliCharlesworth(+1)-你能用更深入的答案解释一下吗?@HotLicks实例化和检查创建了一个固有的race@icepack-我猜您没有看到隐含的
;)。尽管这提出了一个有趣的观点(至少在我的头脑中是如此!);如果另一个类中的静态初始值设定项决定调用
Singleton.getInstance()
?@OliCharlesworth这就是为什么
synchronized
关键字出现在渴望版本的
getInstance
签名中的原因。(我的问题实际上与并发无关;我只是意识到我不知道Java如何定义不同类的静态初始化的相对顺序。)@OliCharlesworth,误解了你的意思。我相信这在omerschleifer的答案中得到了隐式的回答——JIT可能在这里负责一致性(尽管我对Java的知识有限,也许有更了解Java的人可以回答)类的静态初始化本质上是由JVM同步的。类的static init方法中的任何内容都将只执行一次,并且在类变为“可见”之前。如何创建两个单例??如果不同步,两个线程可能都会进行is null检查,查看它是否为真并创建一个新实例静态如何可以有多个值?它不会有两个值。但它仍然可能调用new()两次,甚至可能会将两个不同的对象返回给这两个客户端。关于这个问题有很多信息。如果您愿意,我将向您提供更多信息和链接,请注意,双锁问题主要是Java语言固有的,而不是概念本身