您的位置:首页 > 编程语言 > Java开发

设计模式——单例模式

2016-05-03 17:04 507 查看
上次我们讲到迭代器模式(设计模式——迭代器模式),这次我们来看一个最常用的设计模式——单例模式。单例模式的使用场景很多,比如处理偏好和设置注册表对象、日志对象、对话框等。单例模式是应用最广的设计模式之一,也是很多同学最熟悉的模式,因为它使用简单,很好理解,在一般情况下使用都不会出什么问题。但是如果深入去理解的话,还是会发现有些平时没注意到的问题的,下面就一起学习下巩固下吧。

一、定义

单例模式:确保一个类只有一个实例,而且自行实例化,并向整个系统提供这个实例。

二、UML类图

单例模式UML类图如下:



Client:客户端,使用单例类。

Singleton:单例类。

单例模式的UML图很简单,我们主要的关注点在单例类上。有几个注意点:1⃣,构造方法不对外开放,一般为private,这样就能保证客户端不能new一个单例类实例;2⃣,通过一个静态方法或者枚举返回单例对象;3⃣,确保单例类对象只有一个,特别是在多线程的情况下,这个尤为重要的,这一点也将是我们后面着重讲的;4⃣,确保单例类对象在反序列化时不会重新构建对象,这一点在要实现单例类序列化的情况下要多留意。

三、实例

单例模式的写法也很多种,我们针对一些常见的来一一解释下。

首先见的最多的应该是这个,我们这里称它为version1.0:

public class Singleton {
private static Singleton mInstance = null;

private Singleton() {

}

public static Singleton getInstance() {
if (mInstance == null)
mInstance = new Singleton();
return mInstance;
}
}


这个应该是最好理解的。首先定义一个Singleton的实例mInstance,这实例就是提供给客户端的,通过静态方法getInstance()返回该实例;然后定义的私有构造方法,这样客户端就不能new 一个Singleton实例;静态方法getInstance()也很好理解,判断mInstance是否被实例化,如果没有就通过私有构造方法实例化,最终返回Singleton实例。整个代码内容很好理解,但是在我们上面提到的注意点3⃣,在多线程的环境下这种写法就不能保证还能正常工作了。

我们写个测试来证明下上面的写法在多线程下工作的结果:

public static void main(String[] args){
SingletonTest st = new SingletonTest();
st.testSingleton();
}

public void testSingleton(){
for (int i = 0; i < 100; i++){
new Thread(){
@Override
public void run() {
super.run();
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
getSingletonInstance();
}
}.start();
}
}
private void getSingletonInstance(){
Singleton singleton = Singleton.getInstance();
System.out.println("instance = " + singleton.toString() + " in thread: " + Thread.currentThread().toString());
}


这里只列举了类的内容,在testSingleton()方法中启动了100线程,每个线程中做的就是获取Singleton version1.0单例的实例。然后我们看下运行效果,这里只截取了一部分,我们可以看到有三个不同的实例。需要说明的是这个运行结果不是每次都一样的,这个和线程的调度有关。



上面的version1.0的写法在多线程下不能保证getInstance()方法返回的是同一个实例,我们很快就会想到下面的做法,我们称它为version1.1版本:

public class Singleton {
private static Singleton mInstance = null;

private Singleton(){

}

public synchronized static Singleton getInstance(){
if (mInstance == null)
mInstance = new Singleton();
return mInstance;
}
}


version1.1在version1.0的基础上修改了getInstance()方法,加上了同步锁,这能保证此方法的原子操作,但是这样会带来每次调用getInstance()方法都要同步,这样导致效率很低,所以这个version虽然能工作,但是效率很低。

上面的version1.1在多线程的情况下效率很低,我们再看下面一种写法,我们称它为version1.2

public class Singleton {
private static Singleton mInstance = null;

private Singleton(){

}

public static Singleton getInstance(){
if (mInstance == null){
synchronized (Singleton.class){
if (mInstance == null)
mInstance = new Singleton();
}
}
return mInstance;
}


version1.2的写法叫做double-check,这个版本可以避免前面version1.0和version1.1的问题。我们主要看getInstance()方法,1,判断单例实例mInstance是否为null,不为null,不需要同步直接返回实例,这是first check;2,如果mInstance为null,同步线程,同时会判断mInstance是否为null,为null就创建实例,这是second check。version1.2对于前面的version1.0和version1.1有了很大的改善,不过还有个小细节需要注意。对于变量要不要加上volatile,我个人觉得是不需要的。我们知道volatile(这是在Java 1.5以及之后的版本才有的)关键字有两个作用:1,保证线程中不会存在复本,直接从内存中读取,可以保证多线程环境下该变量修改的可见性;2,禁止重排序优化。对于volatile作用1,mInstance是static的,可以保证线程A修改后,线程B可以看到mInstance已经被修改了,因为他们读取的都是同一个内存地址,这是静态变量的属性,它是独立于实例的。对于,volatile的作用2,虽然“mInstance = new Singleton()”不是原子操作,可能存在重排序优化,但是我们加上了内置锁(synchronized)已经保证了其代码块中的操作的原子性,所以我们也不必担心重排序可能带来的得到不同实例的影响。这是我个人的理解,如有偏差,还望指正!

我们再看另一种写法,我们称它为version1.3

public class Singleton {
private static final Singleton mInstance = new Singleton();

private Singleton(){

}

public static Singleton getInstance(){
return mInstance;
}
}


在version1.3中,单例的实例被定义为static final,在第一次加载到类中就被初始化,所以创建实例本身是线程安全的,但是这样也会带来一个问题:他不是lazy initialization(懒人加载模式),即使客户端不调用getInstace()方法时也会初始化单例实例。

我们再看另一种写法,我们称它为version1.4

public class Singleton {
private static class SingletonHolder{
static final Singleton sInstance = new Singleton();
}
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}


version1.4中,定义了一个静态私有的内部类SingletonHolder,SingletonHolder中定义Singleton实例sInstance为static final,这就使用JVM保证了线程安全;当方法getInstance()第一次被调用时,它第一次读取SingletonHolder.sInstance,使得SingletonHolder得到初始化,如果该方法没有被调用,SingletonHolder就不会被初始化,这样实现了lazy initialization。同时当第二次、第三次….第n次调用getInstance()方法时,得到的仍然是第一次初始化得到的是Singleton实例sInstance。

我们再看最后一种方法,我们称它为version1.5

public enum Singleton {
INSTANCE;
}


这是《Effective Java》所推荐的,此版本是通过枚举来实现的,默认枚举实例的创建时线程安全的。这种方式不但简洁,还提无偿提供了序列化机制,绝对防止多次实例化。

四,总结

单例模式的几种写法就介绍完了,各种版本的优缺点有提到。我个人推荐使用上面的version1.2(double-check)、version1.4、还有version1.5。double-check的方式写起来多了几行代码,当然如果要偷懒的化,version1.5绝对是你的不二之选。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息