单例实例化

下面显示的是单例对象的创建。

public class Map_en_US extends mapTree { private static Map_en_US m_instance; private Map_en_US() {} static{ m_instance = new Map_en_US(); m_instance.init(); } public static Map_en_US getInstance(){ return m_instance; } @Override protected void init() { //some code; } } 

我的问题是使用静态块进行实例化的原因是什么。 我熟悉单身实例化的以下forms。

 public static Map_en_US getInstance(){ if(m_instance==null) m_instance = new Map_en_US(); } 

原因是线程安全

您所熟悉的表单有可能多次初始化单例。 此外,即使在多次初始化之后,未来对不同线程的getInstance()调用也可能会返回不同的实例! 此外,一个线程可能会看到部分初始化的单例实例! (假设构造函数连接到数据库并进行身份validation;一个线程可能在身份validation发生之前获得对单例的引用,即使它是在构造函数中完成的!)

处理线程时遇到一些困难:

  1. 并发 :它们必须同时执行;

  2. 可见性 :一个线程对内存的修改可能对其他线程不可见;

  3. 重新排序 :无法预测执行代码的顺序,这可能会导致非常奇怪的结果。

您应该研究这些困难,以准确理解为什么这些奇怪的行为在JVM中是完全合法的,为什么它们实际上是好的 ,以及如何保护它们。

JVM保证静态块只执行一次(除非你使用不同的ClassLoader加载和初始化类,但是细节超出了这个问题的范围,我会说),并且只有一个线程,并保证其结果对每个其他线程都可见。

这就是你应该在静态块上初始化单例的原因。

我喜欢的模式:线程安全和懒惰

上面的模式将在第一次执行时看到对类Map_en_US的引用时实例化单例(实际上,只有对类本身的引用才会加载它,但可能尚未初始化它;有关更多详细信息,请检查引用)。 也许你不想那样。 也许你只想在第一次调用Map_en_US.getInstance()初始化单例(正如你所说熟悉的模式一样)。

如果这是你想要的,你可以使用以下模式:

 public class Singleton { private Singleton() { ... } private static class SingletonHolder { private static final Singleton instance = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.instance; } } 

在上面的代码中,单例只会在初始化类SingletonHolder时实例化。 这只会发生一次(除非,正如我之前所说,你使用多个ClassLoaders),代码只会被一个线程执行,结果将没有可见性问题,初始化只会在第一次引用SingletonHolder ,这发生在getInstance()方法中。 当我需要单身时,这是我经常使用的模式。

另一种模式……

1. synchronized getInstace()

正如本答案的评论中所讨论的,还有另一种以线程安全的方式实现单例的方法,它与您熟悉的(破坏的)方法几乎相同:

 public class Singleton { private static Singleton instance; public static synchronized getInstance() { if (instance == null) instance = new Singleton(); } } 

上面的代码由内存模型保证是线程安全的。 JVM规范声明如下(以更加神秘的方式):让L成为任何对象的锁,让T1和T2成为两个线程。 在T1被T2获得之前 ,L发生了T1。

这意味着在释放锁之前由T1完成的所有事情在获得相同的锁之后对于每个其他线程都是可见的。

因此,假设T1是进入getInstance()方法的第一个线程。 在它完成之前,没有其他线程能够输入相同的方法(因为它是同步的)。 它将看到instance为null,将实例化Singleton并将其存储在字段中。 然后它将释放锁并返回实例。

然后,等待锁定的T2将能够获取它并输入方法。 由于它获得了与T1刚刚发布的锁相同的锁,因此T2将看到该字段instance包含由T1创建的完全相同的Singleton 实例 ,并将简单地返回它。 更重要的是,由T1完成的单例的初始化发生在T1释放锁定之前,这发生在T2获取锁定之前,因此T2无法看到部分初始化的单例。

上面的代码是完全正确的。 唯一的问题是对单例的访问将被序列化。 如果它发生了很多 ,它将降低应用程序的可伸缩性。 这就是为什么我更喜欢上面展示的SingletonHolder模式:对单例的访问将是真正的并发,而不需要同步!

2.双重检查锁定(DCL)

通常,人们害怕锁定获取的成本。 我已经读到,现在它与大多数应用程序无关。 锁获取的真正问题在于它通过序列化对同步块的访问来损害可伸缩性。

有人设计了一种巧妙的方法来避免锁定,它被称为双重锁定 。 问题是大多数实现都被破坏了 。 也就是说,大多数实现都不是线程安全的(即,与原始问题上的getInstace()方法一样,线程不安全)。

实现DCL的正确方法如下:

 public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized { if (instance == null) { instance = new Singleton(); } } } return instance; } } 

这个正确和不正确的实现之间的区别是volatile关键字。

要理解原因,让T1和T2成为两个线程。 我们首先假设该字段不是易变的。

T1进入getInstace()方法。 它是第一个进入它的人,因此该字段为空。 然后进入同步块,然后进入第二个if。 它也计算为true,因此T1创建单例的新实例并将其存储在字段中。 然后释放锁定,并返回单例。 对于此线程,可以保证Singleton已完全初始化。

现在,T2进入getInstace()方法。 有可能(虽然不能保证)它会看到该instance != null 。 然后它将跳过if块(因此它永远不会获取锁),并将直接返回Singleton的实例。 由于重新排序 ,T2可能无法在其构造函数中看到Singleton执行的所有初始化! 重新访问db connection singleton示例,T2可能会看到已连接但尚未通过身份validation的单例!

了解更多信息…

…我推荐了一本精彩的书, Java Concurrency in Practice ,以及Java语言规范

如果在getInstance()方法中初始化,则可以获得竞争条件,即如果2个线程同时执行if(m_instance == null)检查,则两者都可能看到实例为null,因此两者都可能调用m_instance = new Map_en_US();

由于静态初始化程序块只执行一次(由一个执行类加载器的线程执行),因此您没有问题。

这是一个很好的概述。

这种消除静态块的方法怎么样:

 private static Map_en_US s_instance = new Map_en_US() {{init();}}; 

它做同样的事情,但是更整洁。

这种语法的解释:
外部括号组创建一个匿名类。
内部支撑组称为“实例块” – 它在构造期间发射。
这种语法通常被错误地称为“双括号初始化器”语法,通常是那些不了解正在发生的事情的人。

另外,请注意:
m_实例 (即成员)字段的命名约定前缀。
s_ (即静态)字段的命名约定前缀。
所以我将字段的名称更改为s_...

这取决于init方法的资源密集程度。 如果它做了很多工作,也许你希望在应用程序启动时完成工作而不是第一次调用。 也许它从互联网下载地图? 我不知道…

当JVM首次加载类时,将执行静态块。 正如Bruno所说,这有助于线程安全,因为两个线程不可能首次争夺同一个getInstance()调用。

  1. 使用静态实例化,每个类只有一个实例副本,而不管创建了多少个对象。
  2. 方法的第二个优点是,该方法是thread-safe因为除了返回实例之外,您没有在方法中执行任何操作。

静态块实例化您的类并且只调用一次默认构造函数(如果有),并且应用程序启动时所有静态元素都由JVM加载。

使用getInstance()方法,在调用方法时构建并初始化类的对象,而不是静态初始化。 如果你同时在不同的线程中运行getInstance(),那就不安全了。

static block在这里允许init调用。 其他编码方式可能就像这样(喜欢的是品味问题)

 public class Map_en_US extends mapTree { private static /* thread safe without final, see VM spec 2nd ed 2.17.15 */ Map_en_US m_instance = createAndInit(); private Map_en_US() {} public static Map_en_US getInstance(){ return m_instance; } @Override protected void init() { //some code; } private static Map_en_US createAndInit() { final Map_en_US tmp = new Map_en_US(); tmp.init(); return tmp; } } 

更新每个VM规范2.17.5 更新 ,注释中的详细信息

  // Best way to implement the singleton class in java package com.vsspl.test1; class STest { private static STest ob= null; private STest(){ System.out.println("private constructor"); } public static STest create(){ if(ob==null) ob = new STest(); return ob; } public Object clone(){ STest obb = create(); return obb; } } public class SingletonTest { public static void main(String[] args) { STest ob1 = STest.create(); STest ob2 = STest.create(); STest ob3 = STest.create(); System.out.println("obj1 " + ob1.hashCode()); System.out.println("obj2 " + ob2.hashCode()); System.out.println("obj3 " + ob3.hashCode()); STest ob4 = (STest) ob3.clone(); STest ob5 = (STest) ob2.clone(); System.out.println("obj4 " + ob4.hashCode()); System.out.println("obj5 " + ob5.hashCode()); } } -------------------------------- OUT PUT ------------------------------------- private constructor obj1 1169863946 obj2 1169863946 obj3 1169863946 obj4 1169863946 obj5 1169863946 

有趣的事以前从未见过。 似乎主要是风格偏好。 我认为一个区别是:静态初始化发生在VM启动时,而不是在第一次请求实例时,可能会消除并发实例化的问题? (也可以使用synchronized getInstance()方法声明处理)