2019独角兽企业重金招聘Python工程师标准>>>
定义
确保只有一个类只有一个实例,并提供全局访问点。
为什么要使用它
对一些类来说保证只有一个实例是很重要的。比如windows操作系统中的资源管理器,回收站等工具必须保证只有一个实例,否则系统将会出现一些意想不到的异常。
优点
因为只有一个实例,所以很容易控制它的访问权限;避免了过多的使用静态变量等。
适用范围
当类只能有一个实例而且客户可以从一个众所周知的访问点访问它。
结构(UML)
单例模式的结构比较简单,只有一个类:它的构造器是私有的并且提供了一个返回一个实例的静态方法。
单例模式的UML比较简单:
实现
假设现在你有一个地方需要使用到单例模式,你可能首先会想到这样写:
package com.tony.singleton;
/*** 1、私有化构造器 * 2、提供一个返回实例的方法(全局访问点) * 这种方法叫做懒汉式 */
public class Singleton01 { private static Singleton01 instance = null; //私有化构造器private Singleton01(){ } //静态工厂方法public static Singleton01 getInstance(){ if(instance == null){ instance = new Singleton01(); } return instance; }
}
但是这种方法又有一个问题:现在的程序一般都是多线程的,在并发的情况下可能会出现两个实例!
让我们来分析一下这种情况:现在有两个线程(A、B)同时调用了getInstance()方法,然后一个A线程发现instance为null。当A线程正准备去new一个实例时,B线程也发现instance为null,所以B线程也去new一个实例。这种情况下就会出现两个实例。
怎么解决呢?
此时我们会想这还不简单,给它加一个synchronized同步锁不就OK了?!
package com.tony.singleton; /*** 1、私有化构造器 * 2、提供一个返回实例的方法(全局访问点) */
public class Singleton02 { private static Singleton02 instance = null; //私有化构造器private Singleton02(){ } //静态工厂方法public static synchronized Singleton02 getInstance(){ if(instance == null){ instance = new Singleton02(); } return instance; }
}
OK问题解决了!同步这种方法简单易行解决了线程的并发问题,但同时又带来了一个新的问题:对性能的影响。如果getInstance()频繁被调用,那么你就得重新考虑了:每次调用之前都要给它加同步锁!你要知道同步一个方法可能造成程序执行效率下降100倍,而且只有实例化这个对象时才需要同步。
好吧,既然麻烦都在实例化对象这里,那么我在类加载器加载的时候我就把这个对象实例化了是不是就可以呢?
package com.tony.singleton; /*** * 这种方式就做 饿汉式 */
public class Singleton03 { //当被类加载器加载的时候就把这个类给实例化private static Singleton03 instance = new Singleton03(); //私有化构造器private Singleton03(){ } //静态工厂方法public static Singleton03 getInstance(){ return instance; }
}
利用这种做法,我们依赖JVM在加载这个类时马上创建此唯一的实例。JVM保证在任何线程访问instance变量之前,一定先创建此实例。
这种做法很好,基本上解决了上面的出现的两个问题:1、并发访问;2、对性能的影响。
这种做法适合不太复杂的实例。如果需要实例化的的类很复杂,在创建和运行时方面的负担太重就会增加JVM的负担。
有没有这样的方法:除了解决最初的那两个问题外还能延时加载,减轻JVM的负担?答案是有!
这种方法叫做双重检测锁。这次引进了一个新东西:volatile 关键字。
package com.tony.singleton;/*** 双重检测锁 **/
public class Singleton04 { //增加volatile关键字!!!private volatile static Singleton04 instance = null; //私有化构造器private Singleton04(){ } //静态工厂方法public static synchronized Singleton04 getInstance(){ if(instance == null){ //这段代码仅有一次执行的机会:只有第一次才彻底执行synchronized(Singleton04.class){ if(instance == null){ instance = new Singleton04(); } }} return instance; }
}
1
这时候synchronized所同步的那段代码只会执行一次,而且还保证了延时加载。
总结
实现单例模式的方法还有几种,但比较常用的就是这几种。一般掌握饿汉式、懒汉式和双重检测锁就可以了。
这几种各有优缺点,具体使用那种还有具体情况具体分析。
懒汉式:能够延时加载,但可能对性能影响较大。
懒汉式:对性能影响较小,但不能延时加载。
双重检测锁:能够延时加载,只需同步一次,但是不支持JDK1.4之前的版本。
-----------------------分割线----11.24-------------------------
现在看着一年前自己写的博客,感觉好low~~~
现在有些问题之前没有考虑到的,现在提出来:
- 双重检测锁为什么要加上volatile关键字?如果没有会有什么影响?
- 双重检测锁真的比其他的单例实现更好吗?
针对上述的问题,我来一一分析回答。
- 针对第一个问题,其实当时写博客的时候我也不知道为什么要加volatile关键字,因为书上是这么写的,?。其实这里涉及到一个重排序的问题。什么是重排序?Java虚拟机在执行字节码的时候为了使代码运行时获得更好的性能和效率有权利对字节码进行语义上的重新调整,只要不影响最后的结果即可。下面我举一个例子:
private Object a = new Object();
这是一行再普通不过的代码了,按照我们的经验是从右往左执行的:首先(a)在堆中分配一个Object内存大小的空间,(b)初始化Object实例,然后(c)将引用赋值给a。嗯,没错,一般情况下是这样的。但是在多线程的情况下就不一样了,代码有可能是这样执行的:(a)在堆中分配一个Object内存大小的空间,(c)将引用赋值给a,(b)初始化Object实例。第一步还是一样:分配堆内存,第二步第三步交换了一下顺序。咋一看好像没啥影响啊,但是假如有两条线程(A、B)交替执行就会出问题。继续拿双重检测锁的代码用用,假如A线程首先执行,直到
instance = new Singleton04();
这一行代码,此时发生上述的重排序问题,instance获得引用,但此时内存中实例初始化还没有初始化,现在CPU把A线程切换出去。把B线程切换进来,然后执行这段代码,因为instance变量已经获得了内存的引用条件instance == null 都为false,直到return instance。当其他代码拿到实例,然后执行其方法时,GG,NullPointerException。如果能够理解我刚刚的解释,那么为什么要加volatile关键字也就不难理解了。volatile修饰的变量可以保证任何一个线程对其的改动都会被后面的线程所察觉(happens-before,具体信息参看这里)。
-
针对第二个问题,我主要想表达随着计算机性能的提高,延迟初始化的优点已经显得微不足道。除非这个类真的非常复杂,加载时需要非常多的资源。如果真的是这样,那么我们可能就要反思一下我们的设计是不是出了问题,一个类不能让它承担太多的职责,否则后面维护起来非常麻烦。而且这样也不利用团队协作~所以就现在的计算能力完全不需要延迟初始化,通过使用static关键字,或者使用枚举类不失为一种好的单例实现方式。
好了,如果大家还有什么疑问欢迎留言,我会尽可能的回答大家的问题~
相关知识点
- happens-before
- 重排序
- volatile关键字
参考资料
《Head First 设计模式》
《Java 并发编程实战》
《设计模式》