java面试题网站:www.javaoffers.com
概述:
如果要保证系统里一个类最多只能存在一个实例时,我们就需要单例模式。这种情
况在我们应用中经常碰到,例如缓存池,数据库连接池,线程池,一些应用服务实
例等。在多线程环境中,为了保证实例的唯一性其实并不简单,这章将和读者一起
探讨如何实现单例模式
最简单的单例
public class Singleton {
private static Singleton instance = new Singleton();
//other fields...
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
//other methods...
}
代码注解:
1:Singleton 类的只有一个构造方法,它是被 private 修饰的,客户对象无法创建该
类实例。
2:我们为此单例实现的全局访问点是 public static Singleton getInstance()方法,注
意,instance 变量是私有的,外界无法访问的。读者还可以定义 instance 变量是 public 的,这样把属性直接暴露给其他对象,
就没必要实现 public static Singleton getInstance()方法,但是可读性没有方法来
的直接,而且把该实例变量的名字直接暴露给客户程序,增加了代码的耦合 度,
如果改变此变量名称,会引起客户类的改变。
还有一点,如果该实例需要比较复杂的初始化过程时,把这个过程应该写在
static{...}代码块中。
3:此实现是线程安全的,当多个线程同时去访问该类的getInstance()方法时,不会
初始化多个不同的对象,这是因为,JVM(Java Virtual Machine)在加载此类 时,
对于static属性的初始化只能由一个线程执行且仅一次 6 。
由于此单例提供了静态的公有方法,那么客户使用单例模式的代码也就非常简单 了,
如下所示:
Singleton singleton = Singleton.getInstance();
进阶
延迟创建
如果出于性能等的考虑,我们希望延迟实例化单例对象(Static 属性在加载类是就会
被初始化),只有在第一次使用该类的实例时才去实例化,我们应该怎么办呢?
这个其实并不难做到,我们把单例的实例化过程移至 getInstance()方法,而不在加载
类时预先创建。当访问此方法时,首先判断该实例是不是已经被实例化过了,如果
已被初始化,则直接返回这个对象的引用;否则,创建这个实例并初始化,最后返
回这个对象引用。代码片段如下所示:
public class UnThreadSafeSingelton {
//variables and constructors...
public static UnThreadSafeSingelton getInstance() {
if(instatnce ==null){
instatnce = new UnThreadSafeSingelton();
}
return instatnce;
}
}
我们使用这句 if(instatnce ==null) 判断是否实例化完成了。此方法不是线程安全的,
接下来我们将会讨论。
线程安全
上节我们创建了可延迟初始化的单例,然而不幸的是,在高并发的环境中,
getInstance()方法返回了多个指向不同的该类实例,究竟是什么原因呢?我们 针对
此方法,给出两个线程并发访问 getInstance()方法时的一种情况,如下所示:
如果这两个线程按照上述步骤执行,不难发现,在时刻 1 和 2,由于还没有创建单
例对象,Thread1 和 Thread2 都会进入创建单例实例的代码块分别创建实例。在时刻
3,Thread1 创建了一个实例对象,但是 Thread2 此时已无法知道,继续创建一个新
的实例对象,于是这两个线程持有的实例并非为同一个。更为糟糕的是,在没有自
动内存回收机制的语言平台上运行这样的单例模式,例如使用 C++编写此模式,因
为我们认为创建了一个单例实例,忽略了其他线程所产生的对象,不会手动去回收
它们,引起了内存泄露。
为了解决这个问题,我们给此方法添加 synchronized 关键字,代码如下:
public class ThreadSafeSingelton {
//variables and constructors...
public static synchronized ThreadSafeSingelton getInstance() {
if(instatnce ==null){
instatnce = new ThreadSafeSingelton();
}
return instatnce;
}
}
这样,再多的线程访问都只会实例化一个单例对象。
Double-Check Locking
上述途径虽然实现了多线程的安全访问,但是在多线程高并发访问的情况下,给此
方法加上 synchronized 关键字会使得性能大不如前。我们仔细分析一下不难发现 ,
使用了 synchronized 关键字对整个 getInstance()方法进行同步是没有必要的:我们
只要保证实例化这个对象的那段逻辑被一个线程执行就可以了,而返回引用的那段
代码是没有必要同步的。按照这个想法,我们的代码片段大致如下所示:
public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton instatnce = null;
//constructors
public static DoubleCheckSingleton getInstance() {
if (instatnce == null) {
//check if it is created.
synchronized (DoubleCheckSingleton.class) {
if (instatnce == null)
//synchronize creation block
//double check if it is created
instatnce = new DoubleCheckSingleton();
}
}
return instatnce;
}
}
代码注解:
1:
在 getInstance()方法里,我们首先判断此实例是否已经被创建了,如果还没有创
建,首先使用 synchronized 同步实例化代码块。在同步代码块里,我们还需要
再次检查是否已经创建了此类的实例,这是因为:如果没有第二次检查,这时
有两个线程 Thread A 和 Thread B 同时进入该方法,它们都检测到 instatnce 为
null,不管哪一个线程先占据同步锁创建实例对象,都不会阻止另外一个线程继
续进入实例化代码块重新创建实例对象,这样,同样会生成两个实例对象。所
以,我们在同步的代码块里,进行第二次判断判断该对象是否已被创建。
正是由于使用了两次的检查,我们称之为 double-checked locking 模式。
2:
属性 instatnce 是被 volatile 修饰的,因为 volatile 具有 synchronized 的可见性特
点,也就是说线程能够自动发现 volatile 变量的最新值。这样,如果 instatnce
实例化成功,其他线程便能立即发现。
注意:
此程序只有在 JAVA 5 及以上版 本 才能正常 运 行,在以前版本不能保证其正常运行。
这是由于 Java 平台的内存模式容许 out-of-order writes 引起的,假定有两个线程,
Thread 1 和 Thread 2,它们执行以下步骤:
1.
Thread 1 发现 instatnce 没有被实例化,它获得锁并去实例化此对象, JVM 容许
在没有完全实例化完成时,instance 变量就指向此实例,因为这些步骤可以是
out-of-order writes 的,此时 instance==null 为 false,之前的版本即使用 volatile
关键字修饰也无效。
在初始化完成之前, Thread 2 进入此方法,发现 instance 已经不为 null 了, Thread
2 便认为该实例初始化完成了,使用这个未完全初始化的实例对象,则很可 能引
起系统的崩溃。
Initialization on demand holder
要使用线程安全的延迟的单例初始化,我们还有一种方法,称为 Initialization on
demand holder 模式,代码如下所示:
public class LazyLoadedSingleton {
private LazyLoadedSingleton() {
}
private static class LazyHolder {
//holds the singleton class
private static final LazyLoadedSingleton singletonInstatnce = new
LazyLoadedSingleton();
}
public static LazyLoadedSingleton getInstance() {
return LazyHolder.singletonInstatnce;
}
}
当 JVM 加载 LazyLoadedSingleton 类时,由于该类没有 static 属性,所以加载完成后
便即可返回。只有第一次调用 getInstance()方法时,JVM 才会加载 LazyHolder 类,
由于它包含一个 static 属性 singletonInstatnce,所以会首先初始化这个变量,根据
前面的介绍,我们知道此过程并不会出现并发问题(JLS 保证),这样即实现了一个
既线程安全又支持延迟加载的单例模式。
Singleton 的序列化
如果单例类实现了 Serializable 接口,这时我们得特别注意,因为我们知道在默认情
况下,每次反序列化(Desierialization)总会创建一个新的实例对象,这样一个系统
会出现多个对象供使用。我们应该怎么办呢?
熟悉 Java 序列化的读者可能知道,我们需要在 readResolve()方法里做文章,此方法
在反序列化完成之前被执行,我们在此方法里替换掉反序列化出来的那个新的实 例,
让其指向内存中的那个单例对象即可,代码实现如下:
import java.io.Serializable;
public class SerialibleSingleton implements Serializable {
private static final long serialVersionUID = -6099617126325157499L;
static SerialibleSingleton singleton = new SerialibleSingleton();
27private SerialibleSingleton() {
}
// This method is called immediately after an object of this class is deserialized.
// This method returns the singleton instance.
private Object readResolve() {
return singleton;
}
}
方法 readResolve()直接返回 singleton 单例,这样,我们在内存中始终保持了一个唯
一的单例对象。
转载: 浅谈设计模式书籍