单例模式是指整个应用中某个类只有一个实例出现,并且提供一个全局访问点。
饿汉式(简单粗暴)
这种方法语法非常简单,并且也便于阅读,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
缺点:
- 增加初始化内存消耗(结合实际考虑,大多数情况下此缺点可忽略)
懒汉(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉(线程安全)
上段代码在单线程环境下不存在问题,但是如果处于并发场景,就需要考虑线程安全,最熟悉莫过于双检锁,其要点在于:
- volatile能够提供"可见性",以及保证getInstance返回的是初始化安全的实例
- 在同步之前进行null检查,以尽量避免进入相对昂贵的同步块
- 直接在 class级别进行同步,保证线程安全的类方法调用。
public class Singleton implements Serializable {
//volatile,保证操作可见性和禁止指令重排
private volatile static Singleton instance;
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
/**
* 序列化会通过反射调用无参数的构造方法创建一个新的对象
* 避免序列化重新new 对象 需添加此方法
*/
/*private Object readResolve() {
return Singleton.instance;
}*/
}
在这段代码中,争论较多的是volatile修饰静态变量,当Singleton类本身有多个成员变量时,需要保证初始化过程完成后,才能被get到。
浅说volatile
关于volatile的详细说明,可以参考这篇帖子:Java并发编程:volatile关键字解析
volatile 确保操作是可见性的,每次都是去主内存中读取,还可以避免指令重排(有序性)。看下面这个例子:
int a = 1;//①
int b = 2;//②
int c = a + b;//③
这段代码看似简单,显然的运行顺序是①②③,但是在JVM中运行,这可不一定。这里提一下as-if-serial,as-if-serial是指在执行结果不会改变的情况下,JVM为了提高程序的执行效率会对指令进行重排序(单线程下的执行结果不改变)。如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性。 编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序。从上述代码分析:因为③是一定要在①和②之后执行的,但是①和②则就没有依赖了,①和②谁先执行都不影响结果,所以JVM就可能会对它们进行重排序,所以指令的执行顺序可能是:①②③或②①③
讲完上述例子,我们再看上一个Singleton 代码。instance = new Singleton();
,这段代码并非原子操作,它的执行步骤是:
- 分配空间给对象①
- 在空间内创建对象②
- 将对象赋值给引用instance③
上述过程中,②是依赖于①的,③也是是依赖于①的,所以②在①之后执行,③也在①之后执行,但是③和②不存在依赖性,所以执行顺序可能是:①->③->②或①->②->③,如果是单线程的程序(真的只有一个线程可以访问到它们),那么如果后续程序使用到了instance,JVM会保证你使用instance的时候是初始化完成的,但是现在在synchronized块之外有其它线程“虎视眈眈”,获取到锁的线程如果按照①->③->②的顺序执行,那在执行③的时候会store-write,即将值写回主内存,则其它线程会读到最新的instance值,而现在这个instance指向的是一个不完全的对象,即不安全对象,也不可用,使用这个实例是有危险的,此时构造对象的线程还没有释放锁,其它线程进行第一次检查的时候,null == instance的结果是false,会返回这个对象,造成程序的异常。
一说到指令重排序,我们很容易想到volatile关键字,所以如果instance被volatile修饰的话,可以保证这个初始化的有序性。在现代Java中,内存排序模型(JMM)已经非常完善,通过volatile的write或者read,能保证所谓的happen-before,也就是避免常被提到的指令重排。换句话说,构造对象的store指令能够被保证一定在volatile read之前。
静态内部类
当然,也有一些人推荐利用内部类持有静态对象的方式实现,其理论依据是对象初始化过程中隐含的初始化锁,这种和显视的双检锁实现都能保证线程安全,不过语法稍显晦涩,未必有特别的优势。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举类
public enum EnumSingleton {
INSTANCE;
}
枚举单例写法简单,线程安全,可以防止反射调用构造器,可以防止反序列化的时候创建新的对象(实际也是反射),Effective Java》作者推荐使用的方法。
案例
上面是比较学究的考察,实际实践中未必需要如此复杂,比如java.lang.Runtime中:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {
}
......
}
单例的优缺点
优点:
- 由于单例模式在内存中只有一个是实例,避免了频繁创建对象时的性能开销。
- 避免对资源的多重占用,单例模式可以在系统设置全局的访问点,优化和共享资源访问问。
- 单例可以扩展成有固定数量对象的模式,叫做有上限的多例模式。我们可以在设计之初决定该类在系统中有几个实例,提供系统的处理速度。例如,在连接数据库时,可以预先初始化固定数量的连接,然后在需要并发读取读数据库的时候可以提高系统处理速度。
缺点:
- 单例模式没有接口,不能抽象,扩展困难
- 与单一职责原则有冲突(不一定是很糟糕的)。一个类应该只实现一个逻辑,而不关心它是否是单例的,单例模式把“要单例”和业务逻辑融合在一个类中。