Java singleton

来自WHY42

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

单例模式实现

懒汉模式

在Java编程语言中,单例模式(懒汉模式)应用的例子如下述代码所示 (此种方法只能用在JDK5及以后版本(注意 INSTANCE 被声明为 volatile),之前的版本使用“双重检查锁”会发生非预期行为):

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

另一种写法(线程安全但是效率较低):

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

饿汉模式

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

这种方法有如下的优点:

  • 实例在类加载完成、但是其他线程没有开始使用的时候进行初始化;
  • 不需要对getInstance()方法进行同步,因为所有的线程都会获取到相同的实例,且不需要进行加锁。
  • final关键字保证实例不能被重新定义,确保有且仅有一个实例存在。

static块初始化模式

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

静态内部类

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

使用这种方式的时候,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。

枚举

public enum Singleton {
    INSTANCE;
    public void execute (String arg) {
        // Perform operation here 
    }
}

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

双重检查锁

在早先的Java Memory Model中,下面的实现是存在问题的:

class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) 
      synchronized(this) {
        if (helper == null) 
          helper = new Helper();
      }    
    return helper;
    }
  // other functions and members...
  }

因为对象的初始化分为几个步骤(分配空间,初始化,建立reference等),这些是有可能被reorder的,可能导致的问题是某个线程看到没有完全初始化的对象(即,helper != null但是里面的values都是默认值而不是构造函数中初始化的值)。也就是说,对象还没有完全初始化,就把这个对象的reference给设置好了。

0206106A   mov         eax,0F97E78h
0206106F   call        01F6B210                  ; allocate space for
                                                 ; Singleton, return result in eax
02061074   mov         dword ptr [ebp],eax       ; EBP is &singletons[i].reference 
                                                 ; store the unconstructed object here.
02061077   mov         ecx,dword ptr [eax]       ; dereference the handle to
                                                 ; get the raw pointer
02061079   mov         dword ptr [ecx],100h      ; Next 4 lines are
0206107F   mov         dword ptr [ecx+4],200h    ; Singleton's inlined constructor
02061086   mov         dword ptr [ecx+8],400h
0206108D   mov         dword ptr [ecx+0Ch],0F84030h

As you can see, the assignment to singletons[i].reference is performed before the constructor for Singleton is called. This is completely legal under the existing Java memory model, and also legal in C and C++ (since neither of them have a memory model).

在JDK5引入了新的内存模型之后,就可以使用volatile来fix这个问题了。


If Helper is an immutable object, such that all of the fields of Helper are final, then double-checked locking will work without having to use volatile fields. The idea is that a reference to an immutable object (such as a String or an Integer) should behave in much the same way as an int or float; reading and writing references to immutable objects are atomic.