单例模式详解

125 阅读7分钟

单例模式

单例模式,又叫做 Singleton模式,指的是一个类,在一个JVM里,只有一个实例存在。主要步骤为: 1. 构造方法私有化 2. 静态属性指向实例 3. public static 的 getInstance方法,返回第二步的静态属性

饿汉式

public class Singleton {
    private Singleton(){};
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
}

注意:其中getInstance()的返回值是一个对象的引用,并不是一个新的实例,所以不要错误的理解成多个对象。

懒汉式

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

注意:如果不用synchronized修饰getInstance()方法的话,在单线程中运行是没有问题的,但是在平时的开发中常常会使用多线程,此时这个方法就会出现问题,假设有两个线程A、B,当A线程满足判断还未来得及执行到instance = new Singleton()时,线程执行资格被B拿走,此时线程B进入getInstance(),而此时它也满足instance 的值为null,于是导致最后产生两个实例。

两种方式比较与选用

饿汉式是立即加载的方式,无论是否会用到这个对象,都会加载。 如果在构造方法里写了性能消耗较大,占时较久的代码,比如建立与数据库的连接,那么就会在启动的时候感觉稍微有些卡顿。

懒汉式,是延迟加载的方式,只有使用的时候才会加载。 并且有线程安全的考量。使用懒汉式,在启动的时候,会感觉到比饿汉式略快,因为并没有做对象的实例化。 但是在第一次调用的时候,会进行实例化操作,感觉上就略慢。

因此具体选用看业务需求,如果业务上允许有比较充分的启动和初始化时间,就使用饿汉式,否则就使用懒汉式。

双重校验锁(DCL)

在上述例子当中,每次在调用getInstance()时都需要进行同步,而且在大多数时这种同步是没有必要的,并且大量无用的同步会对性能造成极大的影响。为什么呢?因为在第一次调用getInstance()方法时就已经创建了instance实例了,之后instance就不再为空,然而之后再调用getInstance()时都需要进行同步,从而对性能造成了很大的影响。基于这些问题,一个新的方法也就产生了,这也是我们需要着重讨论的一个方法——双重检查加锁(Double Check Lock)DCL。

//无volatile版本
public class Singleton {
    private Singleton(){};
    private static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

当instance被实例化后再调用getInstance()方法不就不会再进行同步,这样就节约了资源,提升了性能。这里进行了两次判空:第一层主要是为了避免不必要的同步,第二层判断则是为了在null情况下才创建实例。 但是与此同时,这个方法也会带来相应的问题,因为这个方法是含有缺陷的:首先我们看到,DCL方法包含了层判断语句,第一层判断语句用于判断instance对象是否为空,也就是是否被实例化,如果为空时就进入同步代码块进一步判断,问题就出在了instance的实例化语句instance= new Singleton()上,因为这个语句实际上不是原子性的。这句话可以大致分解为如下步骤:

  1. 给Singleton的实例分配内存

  2. 初始化Singleton构造器

  3. 将Singleton实例指向分配的内存空间,此时Singleton实例就不再为空

我们都希望这条语句的执行顺序是上述的1——>2——>3,但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Medel,即Java内存模型)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1——>2——>3也可能是1——>3——>2.如果有两个线程A和B,如果A线程执行完1后先执行3然后执行2,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候instance因为已经在线程A内执行过了第三点,instance已经是非空了,所以线程B直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误很可能会隐藏很久。 因此在JMM的后续版本(Java 5.0及以上)中,如果把instance声明为volatile类型,因为volatile可以防止指令的重排序,那么这样就可以启用DCL,并且这种方式对性能的影响很小,因为volatile变量读取操作的性能通常只是略高于非volatile变量读取操作的性能。改进后的DCL方法如下代码所示。

//把instance声明为volatile类型,防止指令的重排序
public class Singleton {
    private Singleton(){};
    private static volatile Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

关于volatile保证变量可见性: 当一个变量被声明为volatile时,在编译时,会多出一条有lock(锁)前缀的指令,处理器遇到lock指令时会检查数据所在的内存区域,如果该数据是在处理器的内部缓存中,则会锁定此缓存区域,处理完后把缓存写回到主存中,并且会利用缓存一致性协议来保证其他处理器中的缓存数据的一致性。 缓存一致性协议:volatile保证可见性的原理通俗来讲,就是“如果一个共享变量被一个线程修改了之后,当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从自己的工作空间中读取”,实际上是这样的:线程中的处理器会一直在总线上嗅探其内部缓存中的内存地址在其他处理器的操作情况,一旦嗅探到某处处理器打算修改其内存地址中的值,而该内存地址刚好也在自己的内部缓存中,那么处理器就会强制让自己对该缓存地址的无效。所以当该处理器要访问该数据的时候,由于发现自己缓存的数据无效了,就会去主存中访问。 关于volatile保证变量有序性: 另外,volatile关键字能够保证代码的有序性,禁止变量进行重排序,这里需要注意的是,虚拟机只是保证这个变量之前的代码一定比它先执行,但并没有保证这个变量之前的代码不可以重排序。之后的也一样。

延迟初始化占位类模式(静态内部类)

DCL这种方法已经被广泛地遗弃了,因为促使该模式出现的驱动力(无竞争同步的执行速度很慢,以及JVM启动时很慢)已经不复存在,因为它不是一种高效的优化措施。延迟初始化占位类模式能带来同样的优势,并且更容易理解,延迟初始化占位类模式代码如下:

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

总结:在实际开发当中由于经常要考虑到代码的效率和安全性,一般使用饿汉式和延迟初始化占位类模式,而延迟占位类模式更是优势明显并且容易使用和理解,是良好的单例设计模式的实现方法。