题记
深情的告白
老公:老婆,虽然世上有那么多女孩,但你是我心中独一无二的爱人。
(老婆直接一个大嘴巴子抽过去。。。。)
老婆:简单点, 说话的方式简单点,大家都是程序员,你直接说单例模式不就完了吗。
概述
在一些情况下,你希望你得到的对象是单例的,即不管什么时候获得该对象,它都只被初始化过一次,该对象状态的改变对全局都是可见的。在上例中,不管老公在什么地方说自己的老婆,都是指的同一个人,比如她的姓名,省份证号等都是一样的。
想象这样一个场景:一个程序有一个全局配置对象,它有很多字段都有初始默认值,在程序其它很多地方又需要改变该对象的一些字段值,你希望改变后的字段值对全局是可见的,即其它地方在获取该值得时候不再是默认的初始值,而是改变后的值。如果你每次获取该对象都是通过new来产生的,那么显然达不到目的,这个时候就需要单列模式了,它能保证每次你获取的对象都是同一个对象,而且该对象只被初始化过一次。
单列模式
单例模式
单列模式是指:一个类只有一个实例,即只会被初始化一次。类图只有一个类,应该是所有设计模式中最简单的了。从上图中可以看到其要点有3个:
(1)该类持有一个私有的静态的自身变量。static表示该变量只会被初始化一次,private表示该变量不能被该类以外的地方获取,尽管它有static修饰。
(2)该类的构造函数是私有的。代表它不能再其它地方通过new来获取。
(3)改类有一个静态的getInstance方法,它负责返货该类的唯一一个实例。
根据Singleton被初始化的地方不同,单列模式又分为急切式和延迟式:
(1)急切式:在变量的声明处直接实例化。JVM会保证该类在使用前,static变量已完成初始化,所以程序已启动,该变量就已经被初始化了,真的很急切呢。
(2)延迟式:在getInstance方法中实例化变量。该方式实际上把实例化的操作放到了外部调用getInstance方法的时候,所以变量的实例化被延迟了呢。
具体代码如下:
//急切式单例模式
public class Singleton1 {
private static Singleton1 singleton = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstanse(){
return singleton;
}
}
//延迟式单例模式
public class Singleton2 {
private static Singleton2 singleton;
private Singleton2() {
}
public static Singleton2 getInstanse(){
if (null == singleton)
singleton = new Singleton2();
return singleton;
}
}
两种方式的对比:最主要的差别还是第一种是线程安全的,第二种不是线程安全的。这个要怎么理解呢?下面就来讲讲多线程下面的单例模式。
多线程下的单例模式
上面讲的两种方式在单线程下面都能保证程序中单例类只被实例化一次,但是在多线程环境中,急切式任然可保证线程安全,因为JVM会确保程序在调用getInstanse方法时,static的变量已经被初始化。但是延迟式就不能保证线程安全了,原因如下:
如上图所示:关键在于当代码线程1的代码刚执行完if(uniqueInstance == null)还没有执行下一句uniqueInstance = new ChocolateBoiler()的时候,这个时候线程2又刚好执行到if(uniqueInstance == null),发现此时uniqueInstance任然为null,于是也去执行uniqueInstance = new ChocolateBoiler(),这样线程1和线程2都会new一个ChocolateBoiler,这时ChocolateBoiler就不是单例的了。
那如何保证多线程的单例呢?可有以下3中方法:
(1)用急切式的单例模式,如上面所讲。
(2)在延迟式单例模式的getInstance方法上加锁,比如用同步方法或同步代码块。缺点是会降低程序性能。
(3)使用双重检查机制。
延迟式单例模式加同步方法:
public class Singleton2 {
private static Singleton2 singleton;
private Singleton2() {
}
public static synchronized Singleton2 getInstanse(){//同步方法
if (null == singleton)
singleton = new Singleton2();
return singleton;
}
}
下面重点讲讲双重检查机制,具体代码如下:
public class Singleton {
private volatile static Singleton singleton;//1
private Singleton() {
}
public static Singleton getInstanse(){
if (null == singleton){//2
synchronized(Singleton.class){//3
if (null == singleton){//4
singleton = new Singleton();
}
}
}
return singleton;//5
}
}
上述代码中主要讲4处:
(1)在1处增加了volatile关键字,在java中,该关键字表示让JVM不对变量做优化,而且在多线程环境中,该变量的变化对其它线程是可见的,具体如何理解volatile,可以参见:volatile用法详解
(2)在2处,这里的检查singleton是否为null其实只是对singleton被初始化后有效,是只是仅仅用于singleton被实例化后,再次调用getInstance方法时就直接跳到5处,如果是对于还没有被实例化的情况,并不能保证多个线程不同时执行2处if语句里面的代码。
(3)既然2处不是线程安全的,所以3处就加上了同步代码块,以保证线程安全。
(4)对于4处,你可能会问为什么还要再检查一次是否为null。注意,3处的同步代码块只是保证多个线程不能同时执行此处,并不是说只能有一个线程可以执行此处,所以当一个线程完成实例化操作后释放锁,完全有可能其它的线程会再次执行这个代码块,所以还要做一次检查。
到这里,单例模式也就讲完了。
总结
单例模式虽然很简单,但是要注意的细节还是很多的,比如构造函数私有化,变量也要是私有静态的,在多线程下要使用双重检查或者急切式单例模式,很多面试也会考到单例模式,只是我就遇到过。如果这个时候,你用双重检查机制来回答面试官,面试官可能就要刮一下眼睛来看你了----刮目相看。