[关闭]
@pastqing 2016-04-14T20:55:47.000000Z 字数 3195 阅读 3391

java设计模式——单例模式

java 设计模式


单例模式的使用动机

某些系统中的某些类的对象只需要一个, 或者只能是一个, 如果多于一个甚至会出现错误,这时候我们就要用到单例模式。日志对象, 打印池对象, 序列ID生成器等应用。单例模式可以让系统在减少内存空间的情况下仍然能正常工作。


单例模式的几种方式

懒汉式,线程不安全

先说一下何谓懒汉,何谓饿汉。所谓懒汉就是在你要用到这个单例对象的时候才会去建立这个对象, 而饿汉就是无论用不用,都在一开始就建立了这个单例对象。下面是一个典型的懒汉式单例的写法:

  1. public class LazyNotSafe {
  2. private static LazyNotSafe singleton;
  3. private LazyNotSafe() {}
  4. public static LazyNotSafe getInstance() {
  5. if( singleton == null ) {
  6. singleton = new LazyNotSafe();
  7. }
  8. return singleton;
  9. }
  10. }

上面代码很常规, 运用了懒汉式加载, 但是存在的问题是, 在多线程的环境下调用getInstance时就会出现问题,会创建多个实例。原因是线程A在进入if语句块执行创建动作还未结束时, 线程B也进入if语句, 这样就会导致错误的发生。


懒汉式, 线程安全

要解决以上问题, 让线程安全起来, 最简单的方法就是使用synchronized, 将getInstance 声明成一个同步方法即可。

  1. public class LazySafeBad {
  2. private static LazySafeBad singleton = null;
  3. private LazySafeBad() {}
  4. synchronized public static LazySafeBad getInstance() {
  5. if(singleton == null){
  6. singleton = new LazySafeBad();
  7. }
  8. return singleton;
  9. }
  10. }

这样的做法使所有的线程在调用getInstance方法时都会等待其他线程正在调用的线程结束。然而,这种等待的代价显然是太大了。


双重检验锁

上面无脑的同步方法无疑是大大降低了效率, 为此我们使用同步块加锁的方法, 只是需要用到单例对象,且对象还未建立时同步。

  1. public class DoubleLock {
  2. private static DoubleLock singleton = null;
  3. private DoubleLock() {}
  4. public static DoubleLock getInstance() {
  5. if(singleton == null) {
  6. synchronized(DoubleLock.class) {
  7. if(singleton == null) {
  8. singleton = new DoubleLock();
  9. }
  10. }
  11. }
  12. return singleton;
  13. }
  14. }

为什么要判断两次singleton对象是否为空, 原因还是为了避免多个对象实例的生成。假如线程A拿到锁进入同步块生成了一个对象并返回。此时线程B拿到锁进入同步块,如果此时不判断singleton的话,就会多次创建对象, 造成单例失败。

以上的DCL(双重锁)的做法仍然是有问题的。问题出在singleton = new DoubleLock()并不是一个原子操作。在执行new时JVM会发生以下几个动作:

  1. public class DoubleLock {
  2. private volatile static DoubleLock singleton = null;
  3. private DoubleLock() {}
  4. public static DoubleLock getInstance() {
  5. if(singleton == null) {
  6. synchronized(DoubleLock.class) {
  7. if(singleton == null) {
  8. singleton = new DoubleLock();
  9. }
  10. }
  11. }
  12. return singleton;
  13. }
  14. }

关键字volatile可以说是java虚拟机提供的最轻量级的同步机制,它包含了两方面的语义:


饿汉式(立即加载)

  1. public class Hunger {
  2. private static final Hunger singleton = new Hunger();
  3. private Hunger() {}
  4. public static Hunger getInstance() {
  5. return singleton;
  6. }
  7. }

不能有其他实例变量,因为getInstance方法没有同步。否则会出现线程安全性问题。

静态内部类(Initialization On Demand Holder idiom)

  1. public class Singleton {
  2. private Singleton(){}
  3. public static Singleton getInstance(){
  4. return SingletonInstance.instance;
  5. }
  6. private static class SingletonInstance{
  7. static final Singleton instance = new Singleton();
  8. }
  9. }

采取静态内部类的方法实现单例模式的好处是,类第一次加载时,类的静态属性进行初始化, 并且JVM虚拟会保证并发访问, 不会出现初始化过程中遭遇别的线程使用的情况。静态变量只初始化一次, 因此可以保证单例。

对于为什静态内部类采取static final 的写法,其细节说明可以参见一下文章。
Initialization On Demand Holder idiom的实现探讨

枚举

  1. public enum EnumSingleton {
  2. INSTANCE;
  3. //定义枚举的方法
  4. public void someMethods() {
  5. ...
  6. }
  7. }

一个枚举Enum常量代表了一个实例,enum类型只能有这些常量实例。这样的标准保证enum常量不能被克隆,也不会因为反序列化产生不同的实例,想通过反射机制得到一个enum类型的实例也是不行的。因此可以用枚举Enum来实现单例模式。利用枚举实现单例的好处是简单方便, 并且可以传递一些参数,实现一些文件或者流的读取等。同时,枚举的创建在JVM中也是线程安全的,所以不存在同步问题。

参考文献

10 Singleton Pattern Interview questions in Java
How to create thread safe Singleton in Java

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注