@pastqing
2016-04-14T12:55:47.000000Z
字数 3195
阅读 3637
java 设计模式
某些系统中的某些类的对象只需要一个, 或者只能是一个, 如果多于一个甚至会出现错误,这时候我们就要用到单例模式。日志对象, 打印池对象, 序列ID生成器等应用。单例模式可以让系统在减少内存空间的情况下仍然能正常工作。
先说一下何谓懒汉,何谓饿汉。所谓懒汉就是在你要用到这个单例对象的时候才会去建立这个对象, 而饿汉就是无论用不用,都在一开始就建立了这个单例对象。下面是一个典型的懒汉式单例的写法:
public class LazyNotSafe {private static LazyNotSafe singleton;private LazyNotSafe() {}public static LazyNotSafe getInstance() {if( singleton == null ) {singleton = new LazyNotSafe();}return singleton;}}
上面代码很常规, 运用了懒汉式加载, 但是存在的问题是, 在多线程的环境下调用getInstance时就会出现问题,会创建多个实例。原因是线程A在进入if语句块执行创建动作还未结束时, 线程B也进入if语句, 这样就会导致错误的发生。
要解决以上问题, 让线程安全起来, 最简单的方法就是使用synchronized, 将getInstance 声明成一个同步方法即可。
public class LazySafeBad {private static LazySafeBad singleton = null;private LazySafeBad() {}synchronized public static LazySafeBad getInstance() {if(singleton == null){singleton = new LazySafeBad();}return singleton;}}
这样的做法使所有的线程在调用getInstance方法时都会等待其他线程正在调用的线程结束。然而,这种等待的代价显然是太大了。
上面无脑的同步方法无疑是大大降低了效率, 为此我们使用同步块加锁的方法, 只是需要用到单例对象,且对象还未建立时同步。
public class DoubleLock {private static DoubleLock singleton = null;private DoubleLock() {}public static DoubleLock getInstance() {if(singleton == null) {synchronized(DoubleLock.class) {if(singleton == null) {singleton = new DoubleLock();}}}return singleton;}}
为什么要判断两次singleton对象是否为空, 原因还是为了避免多个对象实例的生成。假如线程A拿到锁进入同步块生成了一个对象并返回。此时线程B拿到锁进入同步块,如果此时不判断singleton的话,就会多次创建对象, 造成单例失败。
以上的DCL(双重锁)的做法仍然是有问题的。问题出在singleton = new DoubleLock()并不是一个原子操作。在执行new时JVM会发生以下几个动作:
null了。但是JVM会进行指令的优化重排序, 将原本的1-2-3的顺序可能变为1-3-2。这样就有可能在线程A实例化一个对象之后,线程B在3执行完后就抢占了,此时实例已经非Null此时线程二就会返回该实例, 这样就会出错了。 volatile解决以上问题。
public class DoubleLock {private volatile static DoubleLock singleton = null;private DoubleLock() {}public static DoubleLock getInstance() {if(singleton == null) {synchronized(DoubleLock.class) {if(singleton == null) {singleton = new DoubleLock();}}}return singleton;}}
关键字volatile可以说是java虚拟机提供的最轻量级的同步机制,它包含了两方面的语义:
volatile时,它保证了此变量对所有线程的可见性。这里的可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即知道的。而普通变量无法做到这一点。普通变量的值在线程间传递需要通过主内存来完成。例如,线程A修改了一个普通变量的值,然后向主内存中回写,线程B等线程A回写完成后再次主内存进行读取时,新值才会可见。volatile来解决DLC实现带来的问题, 正是运用了volatile的第二个语义。
public class Hunger {private static final Hunger singleton = new Hunger();private Hunger() {}public static Hunger getInstance() {return singleton;}}
不能有其他实例变量,因为getInstance方法没有同步。否则会出现线程安全性问题。
public class Singleton {private Singleton(){}public static Singleton getInstance(){return SingletonInstance.instance;}private static class SingletonInstance{static final Singleton instance = new Singleton();}}
采取静态内部类的方法实现单例模式的好处是,类第一次加载时,类的静态属性进行初始化, 并且JVM虚拟会保证并发访问, 不会出现初始化过程中遭遇别的线程使用的情况。静态变量只初始化一次, 因此可以保证单例。
对于为什静态内部类采取static final 的写法,其细节说明可以参见一下文章。
Initialization On Demand Holder idiom的实现探讨
public enum EnumSingleton {INSTANCE;//定义枚举的方法public void someMethods() {...}}
一个枚举Enum常量代表了一个实例,enum类型只能有这些常量实例。这样的标准保证enum常量不能被克隆,也不会因为反序列化产生不同的实例,想通过反射机制得到一个enum类型的实例也是不行的。因此可以用枚举Enum来实现单例模式。利用枚举实现单例的好处是简单方便, 并且可以传递一些参数,实现一些文件或者流的读取等。同时,枚举的创建在JVM中也是线程安全的,所以不存在同步问题。
10 Singleton Pattern Interview questions in Java
How to create thread safe Singleton in Java