以下是单例模式的特点:
- 单例类只能有一个实例。
- 单例类必须自己自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
种类的话不好说有几类,因为要考虑到是否在多线程下运行,在多线程下,static并不是线程安全的,如果不做线程安全操作,就不是真正的单例模式了,下面来介绍主要的几类:
懒汉类
//懒汉式单例类.在第一次调用的时候实例化自己
public class Singleton {
private Singleton() {
}
private static Singleton single = null;
//静态工厂方法
public static Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}
懒汉么,所以在多线程下会失效,所以下面介绍三种懒汉的升级版来适应多线程
- 在getinstance前加上synchronized(同步),但这导致的是每次getInstance都会去同步,消耗资源。
public class Singleton {
private Singleton() {
}
private static Singleton single = null;
// 静态工厂方法
public static synchronized Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}
-
然而, synchronized有很大的性能开销. 而且在这里我们是修饰了getInstance方法, 意味着, 如果getInstance被很多线程频繁调用时, 每次都会做同步检查, 会导致程序性能下降.
实际上我们要的是单例, 当单例已经存在的时候, 我们是不需要用同步方法来控制的. 一如我们第一种单例的实现---饿汉模式单例, 我们一开始就创建好了单例, 就无需担心线程同步问题.
但是饿汉模式是提前创建, 那么我们怎么能做到延迟创建, 且线程安全, 且性能有所提升呢?
- 双重检查锁,它是在以上做的修改,判断两次空,所以只有在第一次调用的时候会同步,避免了每次同步资源的消耗,注意
volatile
关键字,在后面有对static和volatile的区分解释;
public class Singleton {
private Singleton() {
}
private volatile static Singleton singleton = null; // 声明成 volatile
//静态工厂方法
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
-
- 舍弃了同步方法
- 在getInstance时, 先检查单例是否已经存在, 如果存在了, 我们无需同步操作了, 任何线程过来直接取单例就行, 大大提升了性能.
- 若单例不存在(第一次调用时), 使用synchronized同步代码块, 来确保进入的只有一个线程, 在此再做一次单例存在与否的检查, 进而创建出单例.
这样就保证了:
- 在单例还没有创建时, 多个线程同时调用getInsance时, 保证只有一个线程能够执行sInstance = new DCLForm()创建单例.
- 在单例已经存在时, getInsance没有加锁, 直接访问, 访问创建好的单例, 从而达到性能提升.
注意
这里我们使用的volatile关键字
具体原因和原理, 请参考这篇文章, 讲的很详细.然而, 使用volatile关键字的双重检查方案需要JDK5及以上(因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义).
那么我们还有什么更通用的方式能保证多线程单例创建, 以及懒加载方式呢?
- 内部静态类,这种我觉得是最好的,既实现了线程安全,也避免了同步带来的性能影响。
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
这种方式, 通过JVM的类加载方式(虚拟机会保证一个类的初始化在多线程环境中被正确的加锁、同步), 来保证了多线程并发访问的正确性.
另外, 由于静态内部类的加载特性 --- 在使用时才加载, 这种方式也达成了懒加载的目的.
显然, 这种方式是一种比较完美的单例模式. 当然, 它也有其弊端, 依赖特定编程语言, 适用于JAVA平台.
饿汉类
饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。
//饿汉式单例类.在类初始化时,已经自行实例化
public class Singleton {
private Singleton() {
}
private static final Singleton single = new Singleton();
//静态工厂方法
public static Singleton getInstance() {
return single;
}
}
这种也是我比较喜欢的,因为简单易懂,但当实现了Serializable接口后,反序列化时单例会被破坏,实现Serializable接口需要重写readResolve,才能保证其反序列化依旧是单例:
private Object readResolve() throws ObjectStreamException {
return single;
}
枚举类
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,很少看见有人这么写过。
以上就是常用的单例模式,一般的情况下,我会使用饿汉式,只有在要明确实现lazy loading效果时才会使用内部静态类,另外,如果涉及到反序列化创建对象时我会试着使用枚举的方式来实现单例,不过,要一直会保证我的程序是线程安全的。
这里附上static和volatile的区别
static是静态的,所以理论上也可以在不同线程去访问,能访问也就是能修改,也是各个业务方可以去全局修改;变量放在主存区上,使用该变量的每个线程,都将从主存区拷贝一份到自己的工作区上进行操作。
volatile是处理多线程锁的替代方案,对应有时需要实时的修改共享资源的变量,被volatile修复的变量的值可以立刻被业务方取得最新的值。
static, 声明这个字段是静态的(可能被多个实例共享),在主存区上该类的所有实例的该字段为同一个变量,即唯一性。
volatile, 声明这个字段易变(可能被多个线程使用),Java内存模型负责各个线程的工作区与主存区的该字段的值保持同步,即一致性。
volatile, 声明变量值的一致性;static,声明变量的唯一性。
此外,volatile同步机制不同于synchronized, 前者是内存同步,后者不仅包含内存同步(一致性),且保证线程互斥(互斥性)。
static 只是声明变量在主存上的唯一性,不能保证工作区与主存区变量值的一致性;除非变量的值是不可变的,即再加上final的修饰符,否则static声明的变量,不是线程安全的