今天我们了解一下单例模式,这个模式似乎是笔试最多的模式之一了(面试几乎必问, 似乎面试官特别感兴趣)
单例,从字面意思看, 就是单独的实例, 表示这个实例是唯一的。
那么很多人就会问了,为什么需要这种只有一个实例的类。
其实,在开发中, 很多对象我们都只需要一个,比如:线程池、缓存等等.
实际上这些对象也有且只能有一个,多个实例的话,反而会有问题,或为程序异常,或为资源不足。
我们先贴一下单例模式的定义: 确保一个类只有一个实例, 并提供一个全局访问点。
单例模式的代码其实很简单,按照正常的思路会这样做。
package com.chris.single;
public class Singleton {
private static Singleton uniqueInstance;
private Singleton(){
}
public static Singleton getInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
定义一个静态变量, 当变量存在就返回,不存在就直接创建一个,这样就只有一个实例了。
如果一般的情况,这段代码好像没有什么问题,能确保只有一个实例。
但是, 这时候又要扯到另一个面试必问的问题了, 多线程!!
当有多个线程去执行获取实例的方法时, 可能会同时会判断到uniqueInstance == null 这个条件,这时候就会同时创建实例,这个类就有两个实例了。
有两个实例就不是only you了, 就不是唯一了,所以我们需要对多线程的情况稍微处理一下。
一到同步的问题,大多数开发人员脑海中立刻回浮现出一个单词 : synchronized!!
的确, 只要用synchronized对方法进行修饰, 多线程的灾难几乎就可以轻易的解决。
package com.chris.single;
public class Singleton {
private static Singleton uniqueInstance;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
synchronized关键字,可以保证一个线程进入这个方法之后,对其加一个线程锁, 其他线程在这个线程离开这个方法之前,
都必须等待,不会有两个线程同时进入这个方法。
这个方法好像是操作起来最快的方法了, 加一个关键字进行修饰就可以解决。
但是,在很多书上都会提及, synchronized关键字, 是一个十分重量级的东西,会很大的影响性能,无论是修饰方法或者同步一个代码块。
所以,如果你的应用程序能够接受这个造成的额外的性能负担, 那么上述代码就是你想要的单例模式, 这个实现方式叫懒加载(lazy-init)!
或者, 你的应用程序在创建和运行时的负担不是很重, 这时候我们也可以使用更简便的方式, 急加载(eagerly-init)!
package com.chris.single;
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return uniqueInstance;
}
}
这种方式会在程序编译后立即加载这个类并创建唯一的实例,而不是在运行时创建, 这样就可以保证只有一个实例。
但是这个类一直都没有被用到的话,这就会浪费空间了,这也是这个方式的一个缺点。
然后如果想综合考虑的话, 这里就有另一种解决办法, 双重校验锁(double-cheked locking)!
package com.chris.single;
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton(){
}
public static Singleton getInstance(){
if(uniqueInstance == null){
synchronized (Singleton.class) {
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
为什么叫双重, 因为他有两个关于同步的关键字, 一个是synchronized, 另一个是volatile.
这个方式会首先检查实例是否被创建,如果未创建,这时候才进行同步,所以,同步只会有一次,相比懒加载方式,这个会大大的减少时间耗费,提高性能。
这个方式好像看起来十分的完美了,但是任何介绍这个双重校验锁方式的读物,都会强调一个事情。
在1.4以及更早的版本中,许多JVM对volatile这个关键字的实现会导致双重校验的失效,所以如果不能用java5以后的版本,就不要用这个方式了(用4以前的几乎没有了吧!)
然后我们聊聊volatile,为什么要用这个关键字呢, 因为虽然synchronized能保证进入创建实例的代码是同步的,但是new Singleton() 这个操作缺却不是原子的。
在创建实例的时候,我们的JVM一般会做三件事:
1. 给uniqueInstance分配内存;
2.调用类的构造方法初始化成员变量;
3.将uniqueInstance对象指向分配的内存空间。
当操作3完成之后,其实这个对象就已经不为null了。
而JVM中的JIT(即时编译器)存在指令重排序的优化,所以,步骤2和步骤3的顺序是不能保证的。
最终可能为1-2-3, 也可能为1-3-2,而如果是后者的话,先完成了内存的分配,而这时候另一个线程进来了,判断实例不为null, 直接返回,
但是这个实例却没有经过第2步初始化, 程序就理所当然的报错了。
volatile这个关键字能保证修饰的参数对所有的线程都是可见的, 并且禁止指令重排序优化。
对所有的线程可见,是表示线程中不会存在该实例的副本,每次需要拿的话都会去主内存中拿。
而禁止指令重排序优化,就如上述所说,能保证他的操作的执行顺序在JIT进行优化时不会被打乱。
在开发中,自己写单例好像也比较少,一般都是用的第三方的,所以只能了解一下大概的概念。
如果在文中有什么错误的地方,还望指正,和大家共勉。。。