单例实例化

pus*_*hya 18 java singleton

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我的问题是使用静态块进行实例化的原因是什么.我熟悉下面的单例实例化形式.

public static Map_en_US getInstance(){
    if(m_instance==null)
      m_instance = new Map_en_US();
}
Run Code Online (Sandbox Code Playgroud)

Bru*_*eis 36

原因是线程安全.

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

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

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

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

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

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

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

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

我的首选模式:线程安全和懒惰

上面的模式将在第一次执行时看到对类的引用时实例化单例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;
  }
}
Run Code Online (Sandbox Code Playgroud)

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

另一种模式......

1. synchronized getInstace()

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

public class Singleton {
  private static Singleton instance;
  public static synchronized getInstance() {
    if (instance == null)
      instance = new Singleton();
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

因此,假设T1是进入该getInstance()方法的第一个线程.在它完成之前,没有其他线程能够输入相同的方法(因为它是同步的).它将看到instancenull,将实例化a 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;
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

欲获得更多信息...

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

  • @Thomas:***小心***双重检查锁定...你将在互联网上找到的大多数实现**都被打破**.要正确完成,请记住该字段必须标记为"volatile"; 如果它不是挥发性的,它必然会被打破. (3认同)