单例(Singleton)模式:
保证一个类在系统里只能有一个对象被实例化。
如:缓存池、数据库链接池、线程池、一些应用服务实例等。
难点:在多线程环境中,保证明例的惟一性。
最简单的单例模式:
- 保证该类构造方法是私有的,外部没法建立该类型的对象;
- 提供一个全局访问点,方便给客户对象提供对此单例对象的使用;
public class Singleton {
/**
* 私有变量,外界没法访问
* 能够定义 public 类型 instance变量,把属性直接暴露给客户对象,则不必实现getInstance()方法
* 可是可读性下降,并且直接暴露实例变量的名字给客户程序,会增长代码的耦合度
*/
private static Singleton instance = new Singleton();
static {
//...
}
// 惟一的 private构造方法,客户对象没法建立该对象实例
private Singleton() {
}
// 全局访问点
public static Singleton getInstance() {
return instance;
}
}
// 客户使用单例模式代码
Singleton singleton = Singleton.getInstance();
若是该实例须要比较复杂的初始化过程时,把这个过程应该写在 static{ ... }代码快中。
注意:此实现是线程安全的,当对个线程同时去访问该类的
getInstance( ) 方法时,不会初始化多个不一样的对象,这是由于,JVM 在加载此类时,对于 static 属性的初始化只能由一个线程执行且仅一次。
进阶:
Statci 在加载类时就会被初始化,出于性能等方面的考虑,咱们但愿延迟实例化单例对象,只有在第一次使用该类的实例时才去实例化。
延迟建立:
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
咱们把单例的实例化过程移至
getInstance( )方法,而不是在加载类时预先建立,当访问此方法时,首先判断该实例是否是已经被实例化过了,若是已被初始化,则直接返回这个对象的引用;不然,建立这个实例并初始化,最后返回这个对象引用。
使用
if (instance == null) 判断是否实例化完成了,此方法不是线程安全的。
线程安全:
在高并发的环境中,getInstance( ) 方法将返回多个指向不一样的该类实例。
|
Thread 1 |
Thread 2 |
1 |
if (instance == null) |
|
2 |
|
if (instance == null) |
3 |
Singleton instance = new Singleton(); |
|
4 |
|
Singleton instance = new Singleton(); |
5 |
return instance; |
|
6 |
|
return instance; |
在时刻 1和 2,因为尚未建立单例对象,Thread 1 和 Thread 2都会进入建立单例实例的代码块分别建立实例。在时刻 3 ,Thread 1建立了一个实例对象,可是 Thread 2此时已没法知道,因而继续建立一个新的实例对象,致使这两个线程持有的实例并不是为同一个。
更为糟糕的是,在没有自动内存回收机制的语言平台上运行这样的单例模式,如:C++,觉得咱们认为建立了一个单例实例,忽略了其余线程所产生的对象,不会手动去回收它们,从而引发内存泄漏。
为了解决这个问题,咱们给次方法添加 关键字,代码以下:
public static synchronized Singleton getInstance() {
if ( instance == null ) {
instance = new Singleton();
}
return instance ;
}
这样,再多的线程访问都只会实例化一个单例对象,实现了多线程的安全访问,可是在多线程高并发访问的状况下,给此方法加上 ynchronized 关键字会是得性能大不如前。
如何建立并发访问效率高的单例:
Double-Check Locking
仔细分析发现,使用
synchronized 关键字对整个 getInstance( ) 方法进行同步是没有必要的:咱们只要保证明例化这个对象的那段逻辑被一个线程执行就能够了,而返回引用的那段代码是没有必要同步的。更改去下:
public static Singleton getInstance() {
if ( instance == null ) {
synchronized (Singleton. class ) {
if ( instance == null ) {
instance = new Singleton();
}
}
}
return instance ;
}
在 getInstance( )方法里,首先判断实例是否已经被建立了,若是尚未建立,首先使用 synchronized 同步实例代码块。在同步代码块里,还须要再次检查是否已经建立了此类的实例,这是由于:若是没有第二次检查,这时有两个线程 Thread A 和 Thread B 同时进入该方法,它们都检测到 instatnce 为 null,无论哪个线程先占据同步锁,并建立实例对象,都不会阻止另一个线程继续进入实例代码块从新建立实例对象,这样,一样会产生两个实例对象。因此,咱们在同步的代码块里,要进行第二次判断,判断该代码是否已被建立。
注意:此程序只有在 JAVA 5及以上版本才能正常运行,在之前版本不能保证其正常运行。这是因为 Java平台的内存模式允许 out-of-order writes 引发的,假定有两个线程,Thread 1 和 Thread 2,它们执行如下步骤:
一、Thread 1发现 instatnce 没有被实例化,它得到锁,并去实例化此对象,JVM 允许在没有彻底实例化完成时,instance 变量就指向此实例,由于这些步骤能够是 out-of-order writes 的,此时 instance==null 为 false,以前的版本即便用 volatile 关键字修饰也无效。
二、在初始化完成以前,Thread 2 进入此方法,发现 instance 已经不为 null了,Thread 2 便认为该实例初始化完成了,使用这个未完成初始化的实例对象,则极可能引发系统的奔溃。
Initialization on demand holder
要使用线程安全的延迟的单例初始化,还有一种方法,代码以下:
public class LazyLoadedSingleton {
private LazyLoadedSingleton {
}
private static class LazyHolder{
private static final LazyLoadedSingleton singletonInstatnce = new LazyLoadedSingleton();
}
public static LazyLoadedSingleton getInstance() {
return LazyHolder.singletonInstatnce;
}
}
当 JVM 加载 LazyLoadedSingleton 类时,因为该类没有 static 属性,因此加载完成后便便可返回。只有第一次调用 getInstance( ) 方法时,JVM 才会加载 LazyHolder 类,因为它包含一个 static 属性 singletonInstatnce,因此会首先初始化这个变量,这样即实现了一个即保证线程安全又支持延迟加载的单例模式。
单例模式序列化应该注意的问题:
Singleton 的序列化
若是单例类实现了 Serializable接口,在默认状况下,每次反序列化总会建立一个新的实例对象,这样一个系统会出现多个对象使用。
解决思路: readResolve( )方法在反序列化完成以前被执行,咱们在此方法里替换掉反序列化出来的那个新的实例,让其指向内存中的那个单例对象便可,代码以下:
public class SerializableSingleton implements Serializable {
private static final long serialVersionUID = 4285441628073602932L;
static SerializableSingleton singleton = new SerializableSingleton();
private SerializableSingleton() {
}
private Object readResolve () {
return singleton ;
}
}
方法 readResovle( ) 直接返回 singleton单例,这样,在内存中始终保持了一个惟一的单例对象。
思考:以上学习,讨论的是在同一个 JVM中,保证一个类只有一个单例,若是在分布式环境中,如何保证在整个应用(可能分布在不一样 JVM上)只有一个实例???