有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

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;
    }
}

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

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

共 (6) 个答案

  1. # 1 楼答案

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

    由于静态初始值设定项块只执行一次(由执行类加载器的一个线程执行),因此没有问题

    Here's a good overview.

  2. # 2 楼答案

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

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

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

    此语法的解释:
    外部大括号集创建一个匿名类
    内部的一组支架称为“实例块”——它在构造过程中激发
    这种语法通常被错误地称为“双大括号初始值设定项”语法,通常是那些不了解情况的人使用的

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

  3. # 3 楼答案

    1. 使用静态实例化,无论创建了多少个对象,每个类只有一个实例副本
    2. 该方法的第二个优点是,该方法是thread-safe,因为除了返回实例之外,您在该方法中不做任何事情
  4. # 4 楼答案

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

  5. # 5 楼答案

    原因是线程安全性

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

    处理线程时存在一些困难:

    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时,才会实例化单例。这只会发生一次(除非,如我前面所说,您使用的是多个类加载器),代码将只由一个线程执行,结果将不会出现可见性问题,并且初始化将只在对SingletonHolder的第一次引用时发生,这发生在getInstance()方法内部。这是我在需要单身时最常使用的模式

    另一种模式

    一,synchronized getInstace()

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

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

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

    这意味着,T1在释放锁之前所做的每一件事,在获得相同的锁之后,其他线程都可以看到

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

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

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

    二,。双重检查锁定(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块(因此它将永远不会获得锁),并将直接返回单例实例。由于重新排序,T2可能无法在其构造函数中看到单例执行的所有初始化!再次查看db连接单例,T2可能会看到一个已连接但尚未验证的单例

    有关更多信息

    。。。我推荐一本精彩的书,Java并发实践,还有Java语言规范

  6. # 6 楼答案

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