浅谈单例模式

单例模式

饿汉模式:全局的单实例在类构建时构建

public class Hungary{
    
    private static final Hungary HUNGARY = new Hungary();
    private Hungary(){} 

    public static Hungary getInstance(){
        return HUNGARY;
    }
}

优点:

  • 饿汉式没有加任何的锁,因此执行效率比较高

缺点:

  • 饿汉式在一开始类加载的时候就实例化,无论使用与否,都会实例化,所以会占据空间,浪费内存,尤其是存在很多需要加载的资源情况下。

枚举模式:自带单例模式,枚举本质也是一个类,在jdk1.5之后就存在

public enum  EnumSingle {

    INSTANCE;
  
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

优点:代码实现简洁清晰。并且她还自动支持序列化机制,绝对防止多次实例化(防反射)。

懒汉模式:在加载类时不创建对象,在需要是在创建对象

public class LazyMan{
    private LazyMan() {
    }
    private static LazyMan lazyMan;
    
    public static LazyMan getInstance(){
        if(lazyMan==null){
            lazyMan=new LazyMan();
        }
        return lazyMan;
    }
}

优点:最基础的实现方式,线程上下文单例,不需要共享给所有线程,也不需要加synchronize之类的锁,以提高性能。

缺点:线程不安全,在单一线程下没有问题,但是多线程就有问题了

public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    LazyMan instance = LazyMan.getInstance();
                    System.out.println(instance);
                }
            }).start();
        }
}

//=======结果=======
com.zunhui.single.Hungary@36bdb2aa
com.zunhui.single.Hungary@4bd8a307
com.zunhui.single.Hungary@36bdb2aa
com.zunhui.single.Hungary@721cca89
//==============

可以发现多次运行结果不一样,所以为了保证安全又诞生了双检索懒汉模式

双检索懒汉模式(DCL):通过加锁保证线程安全。

public class DoubleCheck{
    private DoubleCheck(){}
    //volatile 关键字作用可以是保证可见性或者禁止指令重排
    private volatile static DoubleCheck instance;
    
    public static DoubleCheck getInstance(){
        
        if(instance==null){
            synchronized(DoubleCheck.class){
                if(instance==null){ 
                    instance=new DoubleCheck();
                    /**
                    *new对象分三步
                    1.在内存开辟空间
                    2.调用构造器,初始化对象
                    3.将对象指向内存空间
                    但是,由于new对象不是原子性操作,所以可能存在指令重排执行顺序发生变化
                    a线程 123
                    b线程 132
                    //可能会导致空指针异常,所以需要给变量加volatile关键字
                    */
                }
            }
        } 
        return instance;
    }
}

优点:综合了懒汉式和饿汉式两者的优缺点整合而成,既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。

思考?加锁就一定能保证线程安全嘛?

探究反射破坏单例模式

可以测试通过反射创建对象:

 public static void main(String[] args) throws Exception {
        DoubleCheck instance = DoubleCheck.getInstance();
     //反射创建对象  
     Constructor<DoubleCheck> constructor = DoubleCheck.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        DoubleCheck instance1 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }
//=========结果============
com.zunhui.single.DoubleCheck@45ee12a7
com.zunhui.single.DoubleCheck@330bedb4
//========================

我们可以通过测试看出,通过反射创建了一个新的对象,单例模式被破坏了。于是接着演变:

public class DoubleCheck {

    private DoubleCheck(){
        //在构造器中在加一层判断
        synchronized (DoubleCheck.class){
            if (instance!=null){
                throw new RuntimeException("不能通过反射创建对象~");
            }
        }
    }

    private volatile static DoubleCheck instance;

    public static DoubleCheck getInstance() {

        if (instance == null) {
            synchronized (DoubleCheck.class) {
                if (instance == null) {
                    instance = new DoubleCheck();
                }
            }
        }
        return instance;
    }
}

测试:

public static void main(String[] args) throws Exception {
    DoubleCheck instance = DoubleCheck.getInstance();
    
    Constructor<DoubleCheck> constructor = DoubleCheck.class.getDeclaredConstructor(null);
    constructor.setAccessible(true);
    DoubleCheck instance1 = constructor.newInstance();
    System.out.println(instance);
    System.out.println(instance1);
}
//结果报了异常
java.lang.RuntimeException: 不能通过反射创建对象~

[图片上传失败...(image-e0d83b-1653634504965)]

很明显我们加了三重验证防止反射创建对象,但是,这种情况是我们一开始就调用了getInstance()方法,执行了构造器中的同步代码,如果一开始就使用反射创建对象,那么依旧可以创建,测试一下:

public static void main(String[] args) throws Exception {
    Constructor<DoubleCheck> constructor = DoubleCheck.class.getDeclaredConstructor(null);
    constructor.setAccessible(true);
    DoubleCheck instance = constructor.newInstance();
    DoubleCheck instance1 = constructor.newInstance();
    System.out.println(instance);
    System.out.println(instance1);
}
//结果
com.zunhui.single.DoubleCheck@45ee12a7
com.zunhui.single.DoubleCheck@330bedb4

为了防止这种情况,继续进行优化:

public class DoubleCheck {
    
   //定义一个标志位 可以是任意字符或者进行加密操作
   private static boolean bk = false;

    private DoubleCheck(){
        //同步之后将标志位设为ture 第二次调用构造器就报错,确保只创建一次
        synchronized (DoubleCheck.class){
            if (!bk){
                bk=true;
            }else {
                throw new RuntimeException("不能通过反射创建对象~");
            }
        }
    }

    private volatile static DoubleCheck instance;

    public static DoubleCheck getInstance() {

        if (instance == null) {
            synchronized (DoubleCheck.class) {
                if (instance == null) {
                    instance = new DoubleCheck();
                }
            }
        }
        return instance;
    }
}

继续测试:

 public static void main(String[] args) throws Exception {
        Constructor<DoubleCheck> constructor = DoubleCheck.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        DoubleCheck instance = constructor.newInstance();
        DoubleCheck instance1 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }
//结果报了异常
java.lang.RuntimeException: 不能通过反射创建对象~

说明可以通过加标志位的方式确保构造器只调用一次,只能创建一个对象。但是如果通过反编译等各种手段得到了标志位的话,依旧可以破坏单例模式,继续测试:

public static void main(String[] args) throws Exception {
    //通过反射获得标志位
    Field bk = DoubleCheck.class.getDeclaredField("bk");
    bk.setAccessible(true);

    Constructor<DoubleCheck> constructor = DoubleCheck.class.getDeclaredConstructor(null);
    constructor.setAccessible(true);
    DoubleCheck instance = constructor.newInstance();
    //在创建第一个对象后 恢复标志位
    bk.set(instance,false);
    DoubleCheck instance1 = constructor.newInstance();
    System.out.println(instance);
    System.out.println(instance1);
}
//结果
com.zunhui.single.DoubleCheck@330bedb4
com.zunhui.single.DoubleCheck@2503dbd3

结果我们又破坏了单例模式。

思考?那么,为什么通过反射就能破坏单例模式,就没有反射不能破坏的单例嘛?

我们通过查看constructor.newInstance();的源码:

//其中有这样一个异常,说反射不能创建enum对象
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");

那就说明反射不能创建枚举对象,在jdk中自己设置了不能通过反射创建枚举对象的机制。那我们测试一下:

编写一个枚举类:

public enum  EnumSingle {

    INSTANCE;
    
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

反编译枚举类java文件,查看.class文件:

image-20220527144151075.png

可以看到我们的枚举类本质也是一个类,继承了Enum父类,并且存在无参构造,那我们测试一下通过反射创建对象:

 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle instance = EnumSingle.INSTANCE;
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        //NoSuchMethodException: com.zunhui.single.EnumSingle.
        EnumSingle instance1 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }

结果:

image-20220527144409688.png

结果发现,报了一个不存在无参构造的异常,按理来说应该是报枚举不能被反射创建对象,那这是什么原因呢,我们接着探究,使用jad反编译工具将EnumSingle.class转为EnumSingle.java

public final class EnumSingle extends Enum
{

    public static EnumSingle[] values()
    {
        return (EnumSingle[])$VALUES.clone();
    }

    public static EnumSingle valueOf(String name)
    {
        return (EnumSingle)Enum.valueOf(com/zunhui/single/EnumSingle, name);
    }
    //======================
    private EnumSingle(String s, int i)
    {
        super(s, i);
    }
    //======================
    public EnumSingle getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingle INSTANCE;
    private static final EnumSingle $VALUES[];

    static 
    {
        INSTANCE = new EnumSingle("INSTANCE", 0);
        $VALUES = (new EnumSingle[] {
            INSTANCE
        });
    }
}

通过查看源码,我们发现枚举类确实不存在无参构造,而是存在一个有参构造,两个参数分别是String和int,那我们接着修改测试代码:

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    EnumSingle instance = EnumSingle.INSTANCE;
    //修改为获取有参构造
    Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
    constructor.setAccessible(true);
    //IllegalArgumentException: Cannot reflectively create enum objects
    EnumSingle instance1 = constructor.newInstance();
    System.out.println(instance);
    System.out.println(instance1);
}

结果:

image-20220527144907216.png

符合我们的预期,那就说明枚举类是一个特殊的类,通过有参构造实例化对象,且不能通过反射创建对象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容