设计模式——单例模式
2016-05-03 17:04
507 查看
上次我们讲到迭代器模式(设计模式——迭代器模式),这次我们来看一个最常用的设计模式——单例模式。单例模式的使用场景很多,比如处理偏好和设置注册表对象、日志对象、对话框等。单例模式是应用最广的设计模式之一,也是很多同学最熟悉的模式,因为它使用简单,很好理解,在一般情况下使用都不会出什么问题。但是如果深入去理解的话,还是会发现有些平时没注意到的问题的,下面就一起学习下巩固下吧。
Client:客户端,使用单例类。
Singleton:单例类。
单例模式的UML图很简单,我们主要的关注点在单例类上。有几个注意点:1⃣,构造方法不对外开放,一般为private,这样就能保证客户端不能new一个单例类实例;2⃣,通过一个静态方法或者枚举返回单例对象;3⃣,确保单例类对象只有一个,特别是在多线程的情况下,这个尤为重要的,这一点也将是我们后面着重讲的;4⃣,确保单例类对象在反序列化时不会重新构建对象,这一点在要实现单例类序列化的情况下要多留意。
首先见的最多的应该是这个,我们这里称它为version1.0:
这个应该是最好理解的。首先定义一个Singleton的实例mInstance,这实例就是提供给客户端的,通过静态方法getInstance()返回该实例;然后定义的私有构造方法,这样客户端就不能new 一个Singleton实例;静态方法getInstance()也很好理解,判断mInstance是否被实例化,如果没有就通过私有构造方法实例化,最终返回Singleton实例。整个代码内容很好理解,但是在我们上面提到的注意点3⃣,在多线程的环境下这种写法就不能保证还能正常工作了。
我们写个测试来证明下上面的写法在多线程下工作的结果:
这里只列举了类的内容,在testSingleton()方法中启动了100线程,每个线程中做的就是获取Singleton version1.0单例的实例。然后我们看下运行效果,这里只截取了一部分,我们可以看到有三个不同的实例。需要说明的是这个运行结果不是每次都一样的,这个和线程的调度有关。
上面的version1.0的写法在多线程下不能保证getInstance()方法返回的是同一个实例,我们很快就会想到下面的做法,我们称它为version1.1版本:
version1.1在version1.0的基础上修改了getInstance()方法,加上了同步锁,这能保证此方法的原子操作,但是这样会带来每次调用getInstance()方法都要同步,这样导致效率很低,所以这个version虽然能工作,但是效率很低。
上面的version1.1在多线程的情况下效率很低,我们再看下面一种写法,我们称它为version1.2
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
在version1.3中,单例的实例被定义为static final,在第一次加载到类中就被初始化,所以创建实例本身是线程安全的,但是这样也会带来一个问题:他不是lazy initialization(懒人加载模式),即使客户端不调用getInstace()方法时也会初始化单例实例。
我们再看另一种写法,我们称它为version1.4
version1.4中,定义了一个静态私有的内部类SingletonHolder,SingletonHolder中定义Singleton实例sInstance为static final,这就使用JVM保证了线程安全;当方法getInstance()第一次被调用时,它第一次读取SingletonHolder.sInstance,使得SingletonHolder得到初始化,如果该方法没有被调用,SingletonHolder就不会被初始化,这样实现了lazy initialization。同时当第二次、第三次….第n次调用getInstance()方法时,得到的仍然是第一次初始化得到的是Singleton实例sInstance。
我们再看最后一种方法,我们称它为version1.5
这是《Effective Java》所推荐的,此版本是通过枚举来实现的,默认枚举实例的创建时线程安全的。这种方式不但简洁,还提无偿提供了序列化机制,绝对防止多次实例化。
一、定义
单例模式:确保一个类只有一个实例,而且自行实例化,并向整个系统提供这个实例。二、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绝对是你的不二之选。相关文章推荐
- 举例讲解C#编程中对设计模式中的单例模式的运用
- php设计模式之单例模式实例分析
- PHP基于单例模式实现的数据库操作基类
- JavaScript编程的单例设计模讲解
- C中的volatile使用方法
- C#设计模式之单例模式实例讲解
- Javascript实现单例模式
- JS模式之单例模式基本用法
- 深入理解JavaScript系列(25):设计模式之单例模式详解
- 使用设计模式中的单例模式来实现C++的boost库
- 探讨C语言中关键字volatile的含义
- Java单例模式、饥饿模式代码实例
- java设计优化之单例模式
- 学习Java多线程之volatile域
- Android源码学习之单例模式应用及优点介绍
- C++设计模式之单例模式
- C#窗口实现单例模式的方法
- Java线程安全中的单例模式
- PHP单例模式详细介绍
- PHP 面向对象程序设计(oop)学习笔记(三) - 单例模式和工厂模式