聊聊设计模式之单例模式(下)

前言

在之前的文章《聊聊设计模式之单例模式(上)》中,笔者为大家介绍了单例模式的几种常见的实现方式,并列举了各种实现方式的优缺点。在该文章的最后,笔者指出传统的“双重校验”实现“懒汉模式”的方式中存在的问题,由于篇幅所限,未能详述,因此本文将对这个问题继续深入探讨,并为大家介绍单例模式更优雅的实现方式。

“双重校验”的陷阱
在《聊聊设计模式之单例模式(上)》中,我们讲到因为指令重排序的原因,使得传统的“双重校验”会导致调用方访问到没有完成初始化的单例对象。既然这个问题是指令重排序导致的,那么解决的方案还是得从指令重排序入手。这里主要介绍2种解决方案:

1、禁止指令重排序

2、允许指令重排序,但是不让其他线程“看到”这个重排序的过程

一、基于volatile的“双重检验”
基于volatile的“双重检验”的实现方式非常简单,首先上代码:

public class Singleton {    
private volatile static Singleton singleton;    
private Singleton(){
    }    
public static Singleton getSingleton(){        
    if(singleton==null){            
        synchronized (Singleton.class){                
            if(singleton==null){
                    singleton=new Singleton();
                }
            }
        }        
    return singleton;
    }
}

其实就是把singleton变量声明为volatile型。关于volatile变量后续有时间会再进行介绍。在这里大家只要知道volatile变量在某些情况下会禁止指令重排序。因此实例化singleton对象时的三个步骤:

memory=allocate();  //1.分配对象的内存空间

constructInstance(memory);  //2.初始化对象

singleton= memory;  //3.设置singleton指向刚分配的内存地址

中的第2跟第3步骤不会发生指令重排序。故而在多线程的情况下不会出现某些线程访问到尚未初始化完成的单例对象的问题。

二、基于类初始化的单例模式
Java虚拟机在进行类的加载过程中,会执行类的初始化。在执行初始化期间,Java虚拟机可以同步多个线程对一个类的初始化,保证类的初始化的线程安全性。因此即使在类的初始化过程中存在指令重排序,由于Java虚拟机进行了同步,因此其他线程“看不到”这个重排序的过程。首先上代码:

public class Singleton {    
private Singleton(){
    }    

public static Singleton getSingleton(){        
    return SingletonHolder.singleton;
    }    

private static class SingletonHolder{        
    private static Singleton singleton=new Singleton();
    }
}

在Java中,当某个类的静态字段被使用且该字段不是常量时,将会触发类的初始化,因此当调用getSingleton()方法时将触发SingletonHolder类的初始化,故而能够实现延迟初始化。又因为Java虚拟机规范规定线程在初始化某个类时需要先获取锁,所以可以保证类初始化的线程安全性。

上述单例模式真的是“单例”的吗
写到这里,基于volatile与基于类初始化的单例模式看起来已经十分优雅了,但是上述2种实现方式真的能够保证在任何情况下只创建一个实例对象吗?别忘了,在Java中创建对象的方式可不是只有“new”这一种方式。其实通过反射也能创建对象,以上述基于volatile实现的单例模式为例,我们通过反射创建出另一个对象,首先上代码:

public class Client {    

    public static void main(String[] args) throws Exception{
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton singleton1 = constructor.newInstance();
        System.out.println(singleton1);
        Singleton singleton2=Singleton.getSingleton();
        System.out.println(singleton1==singleton2);
    }
}

该代码的输出如下:

上述代码通过反射创建出了一个新的对象,并与原先的单例对象进行比较,发现两者内存地址不相等,因此是两个不同的对象,故而上述的单例模式看似完美,其实还是有漏洞的。

那么是否有办法防止通过反射创建出对象呢?还真有。接下来我们再介绍另外一种创建单例模式的方法。

基于枚举的单例模式
基于枚举实现单例模式非常简单,首先上代码:

public enum Singleton {

    INSTANCE;
}

代码非常简洁,那么基于枚举实现的单例模式是否可以通过反射创建出新对象呢?我们尝试下就知道了。我们运行以下代码,看看结果输出是什么?其中里面的Singleton类为上述枚举类。

public class Client {    

    public static void main(String[] args) throws Exception{

        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton singleton1 = constructor.newInstance();
        System.out.println(singleton1);

    }
}

结果输出如下:

由此可见基于枚举实现单例模式可以防止通过反射创建对象,但其缺点就是不能延迟初始化。

关于单例模式的介绍到这里就结束了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值