转载请说明来自:http://blog.csdn.net/super_kingking/article/details/51277238
前言:我们在开发过程中都会用到单例设计模式,但是为什么我们要用单例呢?单例设计模式的有点和缺点以及单利设计模式的几种形式?
我们为什么要用单例呢?因为单例可以减轻加载的负担,缩短加载的时间,提高加载的效率。
单例适用的方面:
- 控制资源的使用,通过线程同步来控制资源的并发访问。
- 控制实例的产生,以达到节约资源的目的。
- 通过数据共享,在不建立直接关联的条件下,让多个不相关的线程之间通信。
首先我们先来看一下单例设计模式的写法:
饿汉式设计模式
public class Singleton{
//在自己内部定义自己的一个实例,只供内部调用
private static final Singleton instance = new Singleton();
private Singleton(){
}
//这里提供了一个供外部访问本class的静态方法,可以直接访问
public static Singleton getInstance(){
return instance;
}
}
饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了。但是如果我没有调用getInstance方法,instance实例已经存在,这样造成不必要的资源消耗,肯定希望在需要的时候才去创建instance对象。
懒汉式
public class SingleTon {
//instance需要用static修饰,在static方法中,引用的变量类型必须也是static类型的
private static SingleTon instance;
//构造函数要private修饰,私有化,只能在SingleTon类中创建实例化,避免在外部类中进行实例化。
private SingleTon() {
}
public static SingleTon getInstance() {
if (instance== null) {
instance= new SingleTon();
}
return instance;
}
}
Singleton的唯一实例只能通过getInstance()方法访问。这样的代码如果在多个线程当中是不安全的,所以我们需要进行改进。
**getInstance方法上加同步**
public class SingleTon {
private static SingleTon instance;
private SingleTon() {
}
public static synchronized SingleTon getInstance() {
if (instance == null) {
instance = new SingleTon();
}
return instance ;
}
}
这样的已经很安全,有效的解决了可能会在多个线程中创建出多个instance对象,但是现在又存在一个新的问题,在方法上添加synchronized同步,每次在调用getInstance的时候都会受到同步锁的影响,这样是非常影响效率的。接下来我们再进行改进。
//我们将synchronized加到方法体中
public class SingleTon {
private static SingleTon instance;
private SingleTon() {
}
public static SingleTon getInstance() {
//但是在这里添加和上面的效果其实是一样的,我们还需要进行下一句的优化
synchronized(SingleTon.class) {
if (instance== null) {
instance= new SingleTon();
}
return instance;
}
}
}
DCL (Double Check Lock)双重检查锁定
public class SingleTon {
private static SingleTon instance;
private SingleTon() {
}
public static SingleTon getInstance() {
if(instance== null){
synchronized(SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
return instance;
}
}
}
这种双重锁定的模式,代码再进入第6行的时候,首先会判断instance是否为空,避免了不必要的同步,第2个判断则是为了在null的情况下创建实例,这种方式叫做双重锁定单例。
下面分析一下DCL的步骤,当线程1执行了 instance = new SingleTon()语句,这句话看起来是一句话,但是实际上它做了3件事:
- (1)首先给instance实例分配内存空间(空的内存)
- (2)调用new SingleTon()构造函数,初始化成员字段
- (3)将instance引用指向内存分配的空间地址,此时的instance已经不在是null的了。
JVM编译器的指令重排序,并不是随着代码的一条条顺序执行的,所以会出现1-2-3的执行顺序,也会出现1-3-2的执行顺序,如果是1-3-2的执行顺序,3执行完毕,2未执行前,切换到线程2中去,instance此时已经指向了内存分配的地址,已经不为null,但是返回的是个没有初始化完成的instance对象,这时在线程2中使用就会出错。
在JDK1.5之后修改了这个bug,sun公司提供了volatile关键字:1.可见性:线程在每次使用变量的时候,都会从主内存中读取变量修改后的值。2.阻止指令重排序
将instance修改定义为private volatile static SingleTon instance = null,
public class Singleton {
private Singleton() {} //私有构造函数
private volatile static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null) { //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
当线程1执行 instance = new SingleTon()的时候,JVM的执行顺序必须是
- (1)首先给instance实例分配内存
- (2)调用new SingleTon()构造函数,初始化成员字段
- (3)将instance引用指向内存分配的空间地址。
在线程2看来,instance对象要么指向一个初始化完整的instance对象,要么是null,不会出现中间值。
总结: volatile关键字不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值
静态内部类单例模式:
DCL (Double Check Lock)虽然在一定程度上是解决了资源消耗,多余的同步,线程安全等问题,但是在高并发环境或者jdk1.6版本下使用下也会有一定的缺陷,java内存模型的原因偶尔也是会失败的,但是概率很小。如果使用静态内部类单例模式则安全,高效,唯一:
public class SingleTon {
private SingleTon (){
}
public static Singleton getInstance(){
return SingleTonHolder.singleTon ;
}
private static class SingleTonHolder{
private static final SingleTon singleTon = new SingleTon();
}
}
第一此加载Singleton类时并不会初始化instance,只有在第一次调用getInstance()方法时虚拟机才会加载SingleTonHolder类,然后对instance进行初始化,该形式是利用了ClassLoader的加载机制来实现懒加载,这种方式不仅能够保证线程的安全,而且能够保证单例对象的唯一性,同时还延迟了单例的实例化,所以更推荐这种方式。
缺点:双重检查机制和静态内部类虽然很好,但是依然无法防止利用反射来重复构建对象。
利用反射构建单例对象:
//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));//false
//得到的结果是false,说明这里是2个不同的对象。
那怎么才能阻止反射的构建方式呢?:枚举实现单例设计模式
public enum SingletonEnum {
INSTANCE;
}
使用枚举单例不但能保证防止反射构造对象,而且也能保证线程安全,但是其并不是使用线程安全,单例对象INSTANCE是在枚举类被加载的时候进行初始化的。