单例模式
- 保证一个类仅有一个实例,并提供一个访问它的全局访问点
重点
- 所有类都有构造方法,不编码则系统默认生成空的构造方法,若有显示定义的构造方法,默认的构造方法就会失效。
- 通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例多个对象。一个最好的方法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。
- 单例模式可以实现对唯一实例的受控访问。
懒汉式单例类
在第一次被引用时,才会将自己实例化
- 多线程时的单例,加锁关键字:synchronized
- 双重锁定
class Singleton {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
注意:singleton的变量声明成 volatile。不加的话会有问题,主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
1.给 singleton 分配内存
2.调用 Singleton 的构造函数来初始化成员变量
3.将 singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了)。
但是在即时 JVM 的编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
使用volatile的主要原因是其有个特性:禁止指令重排序优化.也就是说,在volatile变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会重排序到内存屏障之前。 比如上面的例子,读操作必须要等到1-2-3或1-3-2执行完成,不存在1-3然后取值的情况。
饿汉式单例类
单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。
- 这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
//类加载时就初始化
private static final Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
静态内部类 static nested class
class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种写法仍然使用JVM本身机制保证了线程安全。由于静态单例对象没有作为Singleton 的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()是将加载内部类SingletonHolder,在该内部类中定义了一个static类型的变量INSTANCE,此时会首先初始化这个成员变量,由JAVA虚拟机来保证其线程安全性,确保该成员变量只能被初始化一起。由于 getInstance() 方法没有任何线程锁定,因此其性能不会造成任何影响。
由于SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;
同时读取实例的时候不会进行同步,没有性能缺陷;
也不依赖JDK版本;
总结
一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)倾向于使用静态内部类。如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。