单例模式是什么?
单例模式(Singleton)通常来讲,就是让一个类仅仅只有一个实例,并提供一个全局访问点。
如何控制让一个类只能有一个实例对象呢?全局变量?不行,虽然全局变量可以保证一个对象被访问,但是还是无法去阻止你实例化多个对象。
既然外部无法进行控制,那就交给类自己维护吧,而外部仅仅只是起一个“通知的作用”,由类自己负责保存它的唯一实例,并对外提供一个访问该实例的方法。
单例模式有三个特点:
- 单例类由且仅有一个实例。
- 单例类必须自己创建与保存这个唯一实例。
- 单例类必须向外部提供访问这一实例的方法。
为什么要用单例模式?
一个显而易见的好处就是减少资源浪费,因为仅仅只有一个实例被创建,大大节约了资源。
比如我们要加载的一些配置,这些配置存在于整个公共周期,且允许被公共访问,所以只需要维持一份即可。
一个典型的应用如网站在线人数的统计,如何保证所有人实时访问得到的在线人数是一致的呢?最好的办法就是让所有人公共的访问唯一的实例人数,这时单例模式就起到了作用。
还有很多比如数据库连接池的设计,日志的管理,windows的文件管理器等等。
如果出现线程同步并发访问的问题,且该类只需一个实例面向外部访问,那么就要考虑用单例模式。
单例模式的实现
饿汉式实现
class Singleton {
/**
* private限定外部访问,而static保证只存在一个该实例。
* 在初始化的时候就创建实例
*/
private static Singleton instance = new Singleton();
/**
* 构造方法使用private限定外部实例化该类
*/
private Singleton(){}
/**
* 实例的全局访问点
* @return 唯一实例
*/
public static Singleton getInstance() {
return instance;
}
}
我们访问该实例只需要通过全局访问点即可:
Singleton singleton = Singleton.getInstance();
这种实现方法称作饿汉式单例模式。
顾名思义,这种方法在类初始化的时候实例就被创建出来,就像饿了很久的汉子一样,见到一点肉就着急忙慌的抢来吞(笑)。
显而易见的,这种单例模式是线程安全的,因为return
是原子性操作,静态实例的初始化又只进行一次。
但是很容易想到的一个问题就是,如果加载该对象耗时很长而又长时间不使用的话,岂不是性能浪费过大?
而为了解决这个问题,有了下面的懒汉式实现方法。
懒汉式实现
class Singleton {
/**
* private限定外部访问,而static保证只存在一个该实例。
*/
private static Singleton instance;
/**
* 构造方法使用private限定外部实例化该类
*/
private Singleton(){}
/**
* 实例的全局访问点
* @return 唯一实例
*/
public static Singleton getInstance() {
/* 如果没有实例化就实例化,否则返回已有实例 */
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式单例模式只有在第一次调用的时候才会进行实例化。
我不调用,它就不去创建实例,这样性能问题就解决了。
可是,有得必有失,这种单例模式不是线程安全的!
注意实例化的代码:
if (instance == null) {
instance = new Singleton();
}
当有多个线程同时访问的时候,会使得其创建多个实例。
没办法,出现问题,解决问题,为了让它变成线程安全的,我们考虑对其进行加锁处理。
多线程下的单例模式
单重锁定
class Singleton {
/**
* private限定外部访问,而static保证只存在一个该实例。
*/
private static Singleton instance;
/**
* 建立一个静态对象锁
*/
private static final Object syncRoot = new Object();
/**
* 构造方法使用private限定外部实例化该类
*/
private Singleton(){}
/**
* 实例的全局访问点
* @return 唯一实例
*/
public static Singleton getInstance() {
/* 如果没有实例化就实例化,否则返回已有实例 */
synchronized(syncRoot) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
这样虽然可以解决线程不安全的问题,但是每次调用方法都需要synchronized
,这对性能的消耗也是比较大的。
我们可以再对这种模式优化一下。
双重锁定
class Singleton {
/**
* private限定外部访问,而static保证只存在一个该实例。
*/
private volatile static Singleton instance;
/**
* 建立一个静态对象锁
*/
private static final Object syncRoot = new Object();
/**
* 构造方法使用private限定外部实例化该类
*/
private Singleton(){}
/**
* 实例的全局访问点
* @return 唯一实例
*/
public static Singleton getInstance() {
/* 如果没有实例化就实例化,否则返回已有实例 */
if (instance == null) {
/* 先判断是否存在,不存在再进行加锁处理 */
synchronized (syncRoot) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
相比较之前有什么不同呢?会发现有两个点发生了变动:
- 先判断是否存在,再进行加锁处理。
这样如果存在就直接返回,不必再进入同步代码块,大大提高了性能。
也许你会疑问为什么在同步代码块里要判断第二次?
很简单,如果多个线程同时经过第一次判断后等待,没有第二次判断的话,岂不是又要创建很多个实例了?
这样既保证了线程安全,又对性能没有太大的损耗,一举两得~ - 在类属性上加了
volatile
关键字。
简单解释一下volatile
关键字,volatile
保证了共享变量的可见性,也就是说当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。
同时,与volatile
变量有关的所有操作将禁止指令重排序的进行。
(重排序是为了优化程序性能对指令序列进行排序的一种手段)
那么为什么要加volatile
关键字呢?
我们首先来把instance = new Singleton();
分解一下,这一步其实可以分解为几个步骤:
- 为该对象实例分配内存区域。
- 对实例数据进行初始化(不包括静态数据,静态数据在类加载的时候就已经初始化了)。
- 调用构造函数。
- 设置
instance
指向刚分配的地址。
考虑一种情况,就是如果第3、4步发生了指令重排序的话,也就是说先让instance
引用指向刚分配的地址,再调用构造函数。
假设有一个线程A和一个线程B,线程A执行完了第3步(为引用分配地址),时间片结束,线程B执行if (instance == null)
进行判断,发现引用不为null
,就将引用直接返回了,可是这个时候的引用还没有调用构造函数完成初始化,这就是问题所在。
静态内部类实现
单例模式一种推荐的写法是:
class Singleton {
/**
* 构造方法使用private限定外部实例化该类
*/
private Singleton(){}
/**
* 静态内部私有类
* 在第一次加载时初始化并创建实例
*/
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
/**
* 实例的全局访问点
* @return 唯一实例
*/
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
因为第一次加载(调用getInstance()
)才会对内部类进行初始化,所以其实现了延迟加载。
而又因为静态变量只初始化一次,所以其是线程安全的。
这种方法兼并了较高的性能以及线程安全的优点,所以是推荐使用的一种方法。
总结
单例模式主要用于解决数据不一致和性能消耗过大的问题。
它让类自己持有自己的唯一实例,并对外开放一个全局访问点。
单例模式有很多种方法实现,而用哪种方法,还是要根据使用环境来决定。
比如在多线程并发的环境下,我们当然要选线程安全的模式来执行。
而如果实例创建耗费资源较少,耗时短的话,饿汉式单例模式也不妨是一种不错的选择。