为什么需要单例模式
单例模式是自己最先接触的一种设计模式,当时还是开发C++的代码。当时的应用场景是一个控制台程序,对于一个管理资源的类,也涉及初始化、启动等,这样的类只适合构造一个实例,然后不断的复用,保证在运行进程内只有一个实例,便于管理;同时也能减少资源的开销。
从面向对象的概念上讲,我们知道封装,将相关联的东西封装成一个类。在Java中,一种最简单的模式就是POJO(或者叫Java Bean),包括一些属性和对应的getter和setter方法。这种是对属性的封装,通常是比较明显的对应一些实体,他们提供对数据的封装。另外的,就是更常见的类,因为面向对象编程,特别是在Java中,类似的操作或者语义都放在一个类中。
当需要使用类中的方法时,可以声明一个类的实例,每一个类的实例都拥有这个类的非static属性的一份拷贝,方法也是类似。通常的做法是,在需要用的地方声明一个实例,然后用实例去调用对应的方法。但是有的时候,希望这个类只存在一个实例,也就是这个类中的属性只有一份状态,在各处使用时都是对那一份数据的操作。
简单的单例模式
在Java中,常见的模式如下:
public class Singleton {
//私有,静态的一个实例
private static Singleton instance = new Singleton();
// 必须得实现一个私有的无参构造函数,防止调用方直接new实例
private Singleton() {}
// 供使用者调用
public static Singleton getInstance() {
return instance;
}
}
类的使用者要想使用这个类,只有一种方法得到这个类的实例;并且在类的内部,这个实例是静态的,只存在一份。因此,这就保证了“单例”。注意,这样的写法也是线程安全的。
但这中方法有一个问题,就是在类加载时就要初始化,如果如下初始化的内容比较多,加载起来就会比较慢,因此有了新的方法——延迟初始化(Lazy Initialize),如下:
public class Singleton {
//私有,静态的一个实例,先不初始化
private static Singleton instance;
// 必须得实现一个私有的无参构造函数,防止调用方直接new实例
private Singleton() {}
// 供使用者调用
public static Singleton getInstance() {
if (instance != null) {
instance = new Singleton();
}
return instance;
}
}
但是这样又出现了一个问题,就是会导致线程不安全!
其他方案
但是常见的模式在并发环境中会出现问题。
当多个线程同时调用getInstance()方法时,有可能某个线程得到的instance还没有被初始化,这样将不会得到一个初始化好的实例。
首先想的的方法是加同步。
这个时候通常还结合另一种技术——延迟初始化,也就是不采用简单单例模式中的,在声明变量是就初始化的方式,而是等到要用时在进行初始化。
同时,还会加上双重检查,得到的通常模式如下:
class UnSafeSingleton {
private static UnSafeSingleton instance;
private UnSafeSingleton() {}
public static UnSafeSingleton getInstance() {
if (instance == null) {
synchronized (UnSafeSingleton.class) {
if (instance == null) {
instance = new UnSafeSingleton();
}
}
}
return instance;
}
}
可以看到,这种方式加上了同步处理,并且进行了延迟初始化,稍微不注意,以为这个会是线程安全的。
但是,这边还是有问题,就是在给instance赋值时,还是会出现不一致的问题。
一种解决办法是,给instance属性加上volatile特征,这样保证instance对各个线程的可见性,保证只有一份存在。
另一种方法是采用基于类初始化的方法。JVM在类的初始化阶段(即在Class在被加载后,且被线程使用之前),会执行类的初始化。
在执行类的初始化期间,JVM回去获得一个锁。这个锁可以同步多个线程对同一个类的初始化。代码如下:
class SafeSingleton {
private SafeSingleton() {}
private static class SafeSingletonHolder {
public static SafeSingleton intance = new SafeSingleton();
}
public SafeSingleton getInstance() {
return SafeSingletonHolder.intance;
}
}
这样,即使多个线程去调用getInstance()方法,但是在初始化类SafeSingletonHolder时,都能同步进行,因此可以保证线程安全性。
小结
综上所述,单例模式本身很简单,但是如果要是线程安全,还是得有很多考虑。
通过比较发现,基于类初始化的方法比较简单,代码简洁,可以常用。
但是基于volatile的双重检查锁定方法,在延迟初始化非静态实例字段时,唯一可用。