单例实例化 [英] Singleton instantiation

查看:69
本文介绍了单例实例化的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

解决方案

原因是线程安全.

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

处理线程时有一些困难:

  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时实例化单例.这只会发生一次(除非,正如我之前说过的,您正在使用多个ClassLoader),代码将仅由一个线程执行,结果将不会出现可见性问题,并且初始化仅在第一次引用<时发生c4>,它发生在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为两个线程. T2释放L之前,T1释放L.

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

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

然后,正在等待锁的T2将能够获取它并输入方法.由于它获得了与T1刚刚释放的相同的锁,因此T2将看到字段instance包含由T1创建的与Singleton完全相同的实例,并将简单地返回它.而且,由T1完成的单例初始化是在T1释放锁之前发生的,而T2在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()方法.这是有史以来第一个输入它的字段,因此该字段为空.然后进入同步块,然后进入第二个.它还取值为true,因此T1创建一个单例的新实例并将其存储在字段中.然后释放该锁,并返回单例.对于此线程,可以确保Singleton已完全初始化.

现在,T2进入getInstace()方法.有可能(尽管不能保证)看到instance != null.然后它将跳过if块(因此它将永远不会获得锁),并将直接返回Singleton的实例.由于 reordering ,T2可能看不到Singleton在其构造函数中执行的所有初始化!再次查看db连接单例示例,T2可能会看到已连接但尚未通过身份验证的单例!

有关更多信息...

...我会推荐一本精采的书,《 实践中的Java并发性》 ,以及《 Java语言规范》 .

Below show is the creation on the singleton object.

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

My question is what is the reason for using a static block for instantiating. i am familiar with below form of instantiation of the singleton.

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

解决方案

The reason is thread safety.

The form you said you are familiar with has the potential of initializing the singleton a large number of times. Moreover, even after it has been initialized multiple times, future calls to getInstance() by different threads might return different instances! Also, one thread might see a partially-initialized singleton instance! (let's say the constructor connects to a DB and authenticates; one thread might be able to get a reference to the singleton before the authentication happens, even if it is done in the constructor!)

There are some difficulties when dealing with threads:

  1. Concurrency: they have to potential to execute concurrently;

  2. Visibility: modifications to the memory made by one thread might not be visible to other threads;

  3. Reordering: the order in which the code is executed cannot be predicted, which can lead to very strange results.

You should study about these difficulties to understand precisely why those odd behaviors are perfectly legal in the JVM, why they are actually good, and how to protect from them.

The static block is guaranteed, by the JVM, to be executed only once (unless you load and initialize the class using different ClassLoaders, but the details are beyond the scope of this question, I'd say), and by one thread only, and the results of it are guaranteed to be visible to every other thread.

That's why you should initialize the singleton on the static block.

My preferred pattern: thread-safe and lazy

The pattern above will instantiate the singleton on the first time the execution sees a reference to the class Map_en_US (actually, only a reference to the class itself will load it, but might not yet initialize it; for more details, check the reference). Maybe you don't want that. Maybe you want the singleton to be initialized only on the first call to Map_en_US.getInstance() (just as the pattern you said you are familiar with supposedly does).

If that's what you want, you can use the following pattern:

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

In the code above, the singleton will only be instantiated when the class SingletonHolder is initialized. This will happen only once (unless, as I said before, you are using multiple ClassLoaders), the code will be executed by only one thread, the results will have no visibility problems, and the initialization will occur only on the first reference to SingletonHolder, which happens inside the getInstance() method. This is the pattern I use most often when I need a singleton.

Another patterns...

1. synchronized getInstace()

As discussed in the comments to this answer, there's another way to implement the singleton in a thread safe manner, and which is almost the same as the (broken) one you are familiar with:

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

The code above is guaranteed, by the memory model, to be thread safe. The JVM specification states the following (in a more cryptic way): let L be the lock of any object, let T1 and T2 be two threads. The release of L by T1 happens-before the acquisition of L by T2.

This means that every thing that has been done by T1 before releasing the lock will be visible to every other thread after they acquire the same lock.

So, suppose T1 is the first thread that entered the getInstance() method. Until it has finished, no other thread will be able to enter the same method (since it is synchronized). It will see that instance is null, will instantiate a Singleton and store it in the field. It will then release the lock and return the instance.

Then, T2, which was waiting for the lock, will be able to acquire it and enter the method. Since it acquired the same lock that T1 just released, T2 will see that the field instance contains the exact same instance of Singleton created by T1, and will simply return it. What is more, the initialization of the singleton, which has been done by T1, happened before the release of the lock by T1, which happened before the acquisition of the lock by T2, therefore there's no way T2 can see a partially-initialized singleton.

The code above is perfectly correct. The only problem is that the access to the singleton will be serialized. If it happens a lot, it will reduce the scalability of your application. That's why I prefer the SingletonHolder pattern I showed above: access to the singleton will be truly concurrent, without the need for synchronization!

2. Double checked locking (DCL)

Often, people are scared about the cost of lock acquisition. I've read that nowadays it is not that relevant for most applications. The real problem with lock acquisition is that it hurts scalability by serializing access to the synchronized block.

Someone devised an ingenuous way to avoid acquiring a lock, and it has been called double-checked locking. The problem is that most implementations are broken. That is, most implementations are not thread-safe (ie, are as thread-unsafe as the getInstace() method on the original question).

The correct way to implement the DCL is as follows:

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

The difference between this correct and an incorrect implementation is the volatile keyword.

To understand why, let T1 and T2 be two threads. Let's first assume that the field is not volatile.

T1 enters the getInstace() method. It's the first one to ever enter it, so the field is null. It then enters the synchronized block, then the second if. It also evaluates to true, so T1 creates a new instance of the singleton and stores it in the field. The lock is then release, and the singleton is returned. For this thread, it is guaranteed that the Singleton is completely initialized.

Now, T2 enters the getInstace() method. It is possible (although not guaranteed) that it will see that instance != null. It will then skip the if block (and so it will never acquire the lock), and will directly return the instance of the Singleton. Due to reordering, it is possible that T2 will not see all the initialization performed by the Singleton in its constructor! Revisiting the db connection singleton example, T2 might see a connected but not yet authenticated singleton!

For more information...

... I'd recommend a brilliant book, Java Concurrency in Practice, and also, the Java Language Specification.

这篇关于单例实例化的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆