急了急了,破防单例模式

本文主要介绍单例创建的集中方式和反射给单例造成的影响。

单例的定义

单例模式:保证一个类仅有一个实例对象,并且提供一个全局访问点。

单例的特点

  • 单例类只能有一个实例对象
  • 单例类必须自己创建自己的唯一实例
  • 单例类必须对外提供一个访问该实例的方法

使用场景及优点

优:

  • 提供了对唯一实例的受控访问
  • 保证了内存中只有唯一实例,减少内存开销,比如需要多次创建和销毁实例的场景
  • 避免对资源的多重占用,比如文件的写操作

缺:

  • 没有抽象层,接口,不能继承,扩展困难,违反了开闭原则
  • 单例类一般写在同一个类中,职责过重,违背了单一职责原则

应用场景:

文件系统;数据库连接池的设计;日志系统等 IO/生成唯一序列号/身份证/对象需要共享的情况,比如web中配置对象

实现单例

三步:

  1. 构造函数私有化
  2. 在类内部创建实例
  3. 提供本类实例的唯一全局访问点,即唯一实例的方法
饿汉式:
public class Hungry {
    // 构造器私有,静止外部new
    private Hungry(){}

    // 在类的内部创建自己的实例
    private static Hungry hungry = new Hungry();

    // 获取本类实例的唯一全局访问点
    public static Hungry getHungry(){
        return hungry;
    }
}

懒汉式:
public class Lazy1 {
    // 构造器私有,静止外部new
    private Lazy1(){
        System.out.println(Thread.currentThread().getName() + " 访问到了");
    }

    // 定义即可,不真正创建
    private static Lazy1 lazy1 = null;

    // 获取本类实例的唯一全局访问点
    public static Lazy1 getLazy1(){
        // 如果实例不存在则new一个新的实例,否则返回现有的实例
        if (lazy1 == null) {
            lazy1 = new Lazy1();
        }
        return lazy1;
    }

    public static void main(String[] args) {
        // 多线程访问,看看会有什么问题
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Lazy1.getLazy1();
            }).start();
        }
    }
}

单线程环境下是没有问题的,但是多线程的情况下就会出现问题

DCL 懒汉式:

方法上直接加锁:

public static synchronized Lazy1 getLazy1(){
    if (lazy1 == null) {
        lazy1 = new Lazy1();
    }
    return lazy1;
}

缩小锁范围:

public static Lazy1 getLazy1(){
    if (lazy1 == null) {
        synchronized(Lazy1.class){
            lazy1 = new Lazy1();
        }
    }
    return lazy1;
}

双重锁定:

// 获取本类实例的唯一全局访问点
public static Lazy1 getLazy1(){
    // 如果实例不存在则new一个新的实例,否则返回现有的实例
    if (lazy1 == null) {
        // 加锁
        synchronized(Lazy1.class){
            // 第二次判断是否为null
            if (lazy1 == null){
                lazy1 = new Lazy1();
            }
        }
    }
    return lazy1;
}

指令重排序: 指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。

首先要知道 lazy1 = new Lazy1(); 这一步并不是一个原子性操作,也就是说这个操作会分成很多步

① 分配对象的内存空间 ② 执行构造函数,初始化对象 ③ 指向对象到刚分配的内存空间

但是 JVM 为了效率对这个步骤进行了重排序,例如这样:

① 分配对象的内存空间 ③ 指向对象到刚分配的内存空间,对象还没被初始化 ② 执行构造函数,初始化对象

解决的方法很简单——在定义时增加 volatile 关键字,避免指令重排

最终代码:

public class Lazy1 {
    // 构造器私有,静止外部new
    private Lazy1(){
        System.out.println(Thread.currentThread().getName() + " 访问到了");
    }

    // 定义即可,不真正创建
    private static volatile Lazy1 lazy1 = null;

    // 获取本类实例的唯一全局访问点
    public static Lazy1 getLazy1(){
        // 如果实例不存在则new一个新的实例,否则返回现有的实例
        if (lazy1 == null) {
            // 加锁
            synchronized(Lazy1.class){
                // 第二次判断是否为null
                if (lazy1 == null){
                    lazy1 = new Lazy1();
                }
            }
        }
        return lazy1;
    }

    public static void main(String[] args) {
        // 多线程访问,看看会有什么问题
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Lazy1.getLazy1();
            }).start();
        }
    }
}

静态内部类懒汉式单例:

双重锁定算是一种可行不错的方式,而静态内部类就是一种更加好的方法,不仅速度较快,还保证了线程安全,先看代码:

public class Lazy2 {
    // 构造器私有,静止外部new
    private Lazy2(){
        System.out.println(Thread.currentThread().getName() + " 访问到了");
    }

    // 用来获取对象
    public static Lazy2 getLazy2(){
        return InnerClass.lazy2;
    }

    // 创建内部类
    public static class InnerClass {
        // 创建单例对象
        private static Lazy2 lazy2 = new Lazy2();
    }

    public static void main(String[] args) {
        // 多线程访问,看看会有什么问题
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Lazy2.getLazy2();
            }).start();
        }
    }
}

上面的代码,首先 InnerClass 是一个内部类,其在初始化时是不会被加载的,当用户执行了 getLazy2() 方法才会加载,同时创建单例对象,所以他也是懒汉式的方法,因为 InnerClass 是一个静态内部类,所以只会被实例化一次,从而达到线程安全,因为并没有加锁,所以性能上也会很快。

枚举创建单例:

public enum EnumSingle {
    IDEAL;
}

代码就这样,简直不要太简单,访问通过 EnumSingle.IDEAL 就可以访问了


反射破坏单例模式

单例是如何被破坏的:

这是我们原来的写法,new 两个实例出来,输出一下

public class Lazy1 {
    // 构造器私有,静止外部new
    private Lazy1(){
        System.out.println(Thread.currentThread().getName() + " 访问到了");
    }

    // 定义即可,不真正创建
    private static volatile Lazy1 lazy1 = null;

    // 获取本类实例的唯一全局访问点
    public static Lazy1 getLazy1(){
        // 如果实例不存在则new一个新的实例,否则返回现有的实例
        if (lazy1 == null) {
            // 加锁
            synchronized(Lazy1.class){
                // 第二次判断是否为null
                if (lazy1 == null){
                    lazy1 = new Lazy1();
                }
            }
        }
        return lazy1;
    }

    public static void main(String[] args) {

        Lazy1 lazy1 = getLazy1();
        Lazy1 lazy2 = getLazy1();
        System.out.println(lazy1);
        System.out.println(lazy2);

    }
}

运行结果: main 访问到了 cn.ideal.single.Lazy1@1b6d3586 cn.ideal.single.Lazy1@1b6d3586

可以看到,结果是单例没有问题

一个普通实例化,一个反射实例化:
public static void main(String[] args) throws Exception {
    Lazy1 lazy1 = getLazy1();
    // 获得其空参构造器
    Constructor<Lazy1>  declaredConstructor = Lazy1.class.getDeclaredConstructor(null);
    // 使得可操作性该 declaredConstructor 对象
    declaredConstructor.setAccessible(true);
    // 反射实例化
    Lazy1 lazy2 = declaredConstructor.newInstance();
    System.out.println(lazy1);
    System.out.println(lazy2);
}

运行结果:

main 访问到了 main 访问到了 cn.ideal.single.Lazy1@1b6d3586 cn.ideal.single.Lazy1@4554617c

可以看到,单例被破坏了

如何解决:因为我们反射走的其无参构造,所以在无参构造中再次进行非null判断,加上原来的双重锁定,现在也就有三次判断了。
解决方案:增加一个标识位,例如下文通过增加一个布尔类型的 ideal 标识,保证只会执行一次,更安全的做法,可以进行加密处理,保证其安全性。

这样就没问题了吗,并不是,一旦别人通过一些手段得到了这个标识内容,那么他就可以通过修改这个标识继续破坏单例,代码如下(这个把代码贴全一点,前面都是节选关键的,都可以参考这个)

public class Lazy1 {

    private static boolean ideal = false;

    // 构造器私有,静止外部new
    private Lazy1(){
        synchronized (Lazy1.class){
            if (ideal == false){
                ideal = true;
            } else {
                throw new RuntimeException("反射破坏单例异常");
            }
        }
        System.out.println(Thread.currentThread().getName() + " 访问到了");
    }

    // 定义即可,不真正创建
    private static volatile Lazy1 lazy1 = null;

    // 获取本类实例的唯一全局访问点
    public static Lazy1 getLazy1(){
        // 如果实例不存在则new一个新的实例,否则返回现有的实例
        if (lazy1 == null) {
            // 加锁
            synchronized(Lazy1.class){
                // 第二次判断是否为null
                if (lazy1 == null){
                    lazy1 = new Lazy1();
                }
            }
        }
        return lazy1;
    }

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

        Field ideal = Lazy1.class.getDeclaredField("ideal");
        ideal.setAccessible(true);

        // 获得其空参构造器
        Constructor<Lazy1> declaredConstructor = Lazy1.class.getDeclaredConstructor(null);
        // 使得可操作性该 declaredConstructor 对象
        declaredConstructor.setAccessible(true);
        // 反射实例化
        Lazy1 lazy1 = declaredConstructor.newInstance();
        ideal.set(lazy1,false);
        Lazy1 lazy2 = declaredConstructor.newInstance();

        System.out.println(lazy1);
        System.out.println(lazy2);

    }
}

运行结果: main 访问到了 main 访问到了 cn.ideal.single.Lazy1@4554617c cn.ideal.single.Lazy1@74a14482 实例化 lazy1 后,其执行了修改 ideal 这个布尔值为 false,从而绕过了判断,再次破坏了单例 所以,可以得出,这几种方式都是不安全的,都有着被反射破坏的风险。

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