单例模式,你真的写对了吗?

看公司代码的时候发现项目中单例模式应用挺多的,并且发现的两处单例模式用的还是不同的方式实现的,那么单例模式到底有几种写法呢?单例模式看似很简单,但是实际写起来却问题多多

本文大纲

  • 什么是单例模式
  • 饿汉式创建单例对象
  • 懒汉式创建单例对象
  • 单例模式的优缺点
  • 单例模式的应用场景

什么是单例模式

确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例,并且有两种创建方式,一种是饿汉式创建,另外一种是懒汉式创建

饿汉式创建单例模式

饿汉式创建就是在类加载时就已创建好对象,而不是在需要时在创建对象

public class HungrySingleton {
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    /**
     * 私有构造函数,不能被外部所访问
     */
    private HungrySingleton() {}

    /**
     * 返回单例对象
     * */
    public static HungrySingleton getHungrySingleton() {
        return hungrySingleton;
    }
}

说明:

  • 构造函数私有化,保证外部不能调用构造函数创建对象,创建对象的行为只能由这个类决定
  • 只能通过getHungrySingleton方法获取对象
  • HungrySingleton对象已经创建完成【在类加载时创建】

缺点:

  • 如果getHungrySingleton一直没有被使用到,有点浪费资源

优点:

  • ClassLoad保证线程安全

懒汉式创建单例模式

懒汉式创建就是在第一次需要该对象时在创建

  • 存在错误的懒汉式创建单例对象
    根据定义很容易在上面饿汉式的基础上进行修改

    public class LazySingleton {
        private static LazySingleton lazySingleton = null;
    
        /**
         * 构造函数私有化
         * */
        private LazySingleton() {
        }
    
        private static LazySingleton getLazySingleton() {
            if (lazySingleton == null) {
                return new LazySingleton();
            }
    
            return lazySingleton;
        }
    }
    

    说明:

    • 构造函数私有化
    • 当需要时【getLazySingleton方法调用时】才创建
      嗯,好像没什么问题,但是当有多个线程同时调用getLazySingleton方法时,此时刚好对象没有初始化,两个线程同时通过lazySingleton == null的校验,将会创建两个LazySingleton对象。必须搞点手段使getLazySingleton方法是线程安全的
  • synchronizeLock
    很容易想到使用synchronizeLock对方法进行加锁
    使用synchronize

    public class LazySynchronizeSingleton {
        private static LazySynchronizeSingleton lazySynchronizeSingleton= null;
    
        /**
         * 构造函数私有化
         * */
        private LazySynchronizeSingleton() {
        }
    
        public synchronized static LazySynchronizeSingleton getLazySynchronizeSingleton() {
            if (lazySynchronizeSingleton == null) {
                lazySynchronizeSingleton = new LazySynchronizeSingleton();
            }
    
            return lazySynchronizeSingleton;
        }
    }
    

    使用Lock

    public class LazyLockSingleton {
        private static LazyLockSingleton lazyLockSingleton = null;
    
        /**
        * 锁
        **/
        private static Lock lock = new ReentrantLock();
    
        /**
         * 构造函数私有化
         * */
        private LazyLockSingleton() {
        }
    
        public static LazyLockSingleton getLazyLockSingleton() {
            try {
                lock.lock();
                if (lazyLockSingleton == null) {
                    lazyLockSingleton = new LazyLockSingleton();
                }
            } finally {
                lock.unlock();
            }
    
            return lazyLockSingleton;
        }
    }
    

    这两种方式虽然保证了线程安全,但是性能较差,因为线程不安全主要是由这段代码引起的:

    if (lazyLockSingleton == null) {
      lazyLockSingleton = new LazyLockSingleton();
    }
    

    给方法加锁无论对象是否已经初始化都会造成线程阻塞。如果对象为null的情况下才进行加锁,对象不为null的时候则不进行加锁,那么性能将会得到提升,双重锁检查可以实现这个需求

  • 双重锁检查

在加锁之前先判断lazyDoubleCheckSingleton == null是否成立,如果不成立直接返回创建好的对象,成立在加锁

public class LazyDoubleCheckSingleton {
    /**
     * 使用volatile进行修饰,禁止指令重排
     * */
    private static volatile LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    /**
     * 构造函数私有化
     * */
    private LazyDoubleCheckSingleton() {
    }

    public static LazyDoubleCheckSingleton getLazyDoubleCheckSingleton() {
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }

        return lazyDoubleCheckSingleton;
    }
}

说明:

  • 为什么需要对lazyDoubleCheckSingleton添加volatile修饰符
    因为lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();不是原子性的,分为三步:
    • lazyDoubleCheckSingleton分配内存
    • 调用构造函数进行初始化
    • lazyDoubleCheckSingleton对象指向分配的内存【执行完这步lazyDoubleCheckSingleton将不为null】为了提高程序的运行效率,编译器会进行一个指令重排,步骤2和步骤三进行了重排,线程1先执行了步骤一和步骤三,执行完后,lazyDoubleCheckSingleton不为null,此时线程2执行到if (lazyDoubleCheckSingleton == null),线程2将可能直接返回未正确进行初始化的lazyDoubleCheckSingleton对象。出错的原因主要是lazyDoubleCheckSingleton未正确初始化完成【写】,但是其他线程已经读取lazyDoubleCheckSingleton的值【读】,使用volatile可以禁止指令重排序,通过内存屏障保证写操作之前不会调用读操作【执行if (lazyDoubleCheckSingleton == null)

缺点:

  • 为了保证线程安全,代码不够优雅过于臃肿

  • 静态内部类

    public class LazyStaticSingleton {
        /**
         * 静态内部类
         * */
        private static class LazyStaticSingletonHolder {
            private static LazyStaticSingleton lazyStaticSingleton = new LazyStaticSingleton();
        }
    
        /**
         * 构造函数私有化
         * */
        private LazyStaticSingleton() {
        }
    
        public static LazyStaticSingleton getLazyStaticSingleton() {
            return LazyStaticSingletonHolder.lazyStaticSingleton;
        }
    }
    

    静态内部类在调用时才会进行初始化,因此是懒汉式的,LazyStaticSingleton lazyStaticSingleton = new LazyStaticSingleton();看似是饿汉式的,但是只有调用getLazyStaticSingleton时才会进行初始化,线程安全由ClassLoad保证,不用思考怎么加锁

前面几种方式实现单例的方式虽然各有优缺点,但是基本实现了单例线程安全的要求。但是总有人看不惯单例模式勤俭节约的优点,对它进行攻击。对它进行攻击无非就是创建不只一个类,java中创建对象的方式有newclone、序列化、反射。构造函数私有化不可能通过new创建对象、同时单例类没有实现Cloneable接口无法通过clone方法创建对象,那剩下的攻击只有反射攻击和序列化攻击了
反射攻击:

public class ReflectAttackTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //静态内部类
        LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton();
        //通过反射创建LazyStaticSingleton
        Constructor<LazyStaticSingleton> constructor = LazyStaticSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazyStaticSingleton lazyStaticSingleton1 = constructor.newInstance();
        //打印结果为false,说明又创建了一个新对象
        System.out.println(lazyStaticSingleton == lazyStaticSingleton1);

        //synchronize
        LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
        Constructor<LazySynchronizeSingleton> lazySynchronizeSingletonConstructor = LazySynchronizeSingleton.class.getDeclaredConstructor();
        lazySynchronizeSingletonConstructor.setAccessible(true);
        LazySynchronizeSingleton lazySynchronizeSingleton1 = lazySynchronizeSingletonConstructor.newInstance();
        System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);

        //lock
        LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
        Constructor<LazyLockSingleton> lazyLockSingletonConstructor = LazyLockSingleton.class.getDeclaredConstructor();
        lazyLockSingletonConstructor.setAccessible(true);
        LazyLockSingleton lazyLockSingleton1 = lazyLockSingletonConstructor.newInstance();
        System.out.println(lazyLockSingleton == lazyLockSingleton1);

        //双重锁检查
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
        Constructor<LazyDoubleCheckSingleton> lazyDoubleCheckSingletonConstructor = LazyDoubleCheckSingleton.class.getDeclaredConstructor();
        lazyDoubleCheckSingletonConstructor.setAccessible(true);
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton1 = lazyDoubleCheckSingletonConstructor.newInstance();
        System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton1);
    }
}

都存在反射攻击,都可以创建出一个新对象,打印结果都为false。针对存在的反射攻击根据网上提供的思路在抢救一下,抢救姿势如下:

 private LazySynchronizeSingleton() {
      //flag为线程间共享,进行加锁控制
      synchronized (LazySynchronizeSingleton.class) {
          if (flag == false) {
              flag = !flag;
          } else {
              throw new RuntimeException("单例模式被攻击");
          }
      }
  }

构造函数只能调用一次,调用第二次将抛出异常,通过flag来判断构造函数是否已经被调用过一次了。但是我们仍可以通过反射修改flag的值:

//调用反射前将flag设置为false
Field flagField = lazySynchronizeSingleton.getClass().getDeclaredField("flag");
flagField.setAccessible(true);
flagField.set(lazySynchronizeSingleton, false);

抢救失败,你可能想通过final修饰禁止修改,但是反射可以先去除final,在加上final修改值,对于反射攻击,无力回天,只能选择不适用存在反射攻击的单例创建方式

反序列化攻击:

public class SerializableAttackTest {
    public static void main(String[] args) {
        //懒汉式
        HungrySingleton hungrySingleton = HungrySingleton.getHungrySingleton();
        //序列化
        byte[] serialize = SerializationUtils.serialize(hungrySingleton);
        //反序列化
        HungrySingleton hungrySingleton1 = SerializationUtils.deserialize(serialize);
        System.out.println(hungrySingleton == hungrySingleton1);

        //双重锁
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
        byte[] serialize1 = SerializationUtils.serialize(lazyDoubleCheckSingleton);
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton11 = SerializationUtils.deserialize(serialize1);
        System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton11);

        //lock
        LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
        byte[] serialize2 = SerializationUtils.serialize(lazyLockSingleton);
        LazyLockSingleton lazyLockSingleton1 = SerializationUtils.deserialize(serialize2);
        System.out.println(lazyLockSingleton == lazyLockSingleton1);

        //synchronie
        LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
        byte[] serialize3 = SerializationUtils.serialize(lazySynchronizeSingleton);
        LazySynchronizeSingleton lazySynchronizeSingleton1 = SerializationUtils.deserialize(serialize3);
        System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);

        //静态内部类
        LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton();
        byte[] serialize4 = SerializationUtils.serialize(lazySynchronizeSingleton);
        LazyStaticSingleton lazyStaticSingleton1 = SerializationUtils.deserialize(serialize4);
        System.out.println(lazyStaticSingleton == lazyStaticSingleton1);

    }
}

打印结果都为false,都存在反序列化攻击
对于反序列化攻击,还是有有效的抢救方式的,抢救姿势如下:

private Object readResolve() {
    return lazySynchronizeSingleton;
}
复制代码

添加readResolve方法并返回创建的单例对象,至于抢救的原理,可以通过跟进SerializationUtils.deserialize的代码可知
上述实现单例对象的方式既要考虑线程安全、又要考虑攻击,而通过枚举创建单例对象完全不用担心这些问题

  • 枚举

    public enum EnumSingleton {
        INSTANCE;
    
        public static EnumSingleton getEnumSingleton() {
            return INSTANCE;
        }
    }
    

    代码实现也相当优美,总共才8行代
    实现原理:枚举类的域(field)其实是相应的enum类型的一个实例对象
    可以参考:implementing-singleton-with-an-enum-in-java
    枚举攻击测试:

    public class EnumAttackTest {
      public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
          EnumSingleton enumSingleton = EnumSingleton.getEnumSingleton();
          //序列化攻击
          byte[] serialize4 = SerializationUtils.serialize(enumSingleton);
          EnumSingleton enumSingleton2 = SerializationUtils.deserialize(serialize4);
          System.out.println(enumSingleton == enumSingleton2);
    
          //反射攻击
          Constructor<EnumSingleton> enumSingletonConstructor = EnumSingleton.class.getDeclaredConstructor();
          enumSingletonConstructor.setAccessible(true);
          EnumSingleton enumSingleton1 = enumSingletonConstructor.newInstance();
          System.out.println(enumSingleton == enumSingleton1);
      }
    }
    

    反射攻击将会抛出异常,序列化攻击对它无效,打印结果为true,用枚举创建单例对象真的是无懈可击

单例模式的优点

  • 只创建了一个实例,节省内存开销
  • 减少了系统的性能开销,创建对象回收对象对性能都有一定的影响
  • 避免对资源的多重占用
  • 在系统设置全局的访问点,优化和共享资源优化

总结一下就是节约资源、提升性能

单例模式的缺点

  • 不适用于变化的对象
  • 单例模式中没有抽象层,扩展有困难
  • 与单一原则冲突。一个类应该只实现一个逻辑,而不关心它是否单例,是不是单例应该由业务决定

单例模式的应用场景

  • Spring IOC默认使用单例模式创建bean
  • 创建对象需要消耗的资源过多时
  • 需要定义大量的静态常量和静态方法的环境,比如工具类【感觉是最常见应用场景】

小结

总共介绍了六种正确创建单例对象的方式,推荐使用饿汉式创建单例对象的方式,如果对资源使用有要求,则推荐使用静态内部类【注意反序列化攻击】,其他方式在保证线程安全的同时对性能将会有影响。枚举类其实是非常不错的,线程安全、不存在反射攻击和反序列化攻击,但是感觉这种创建单例方式应用较少,公司代码中使用的是双重锁检查和静态内部类【存在反序列化攻击】创建单例方式,甚至之前出去面试时面试官让写一个单例,我使用的是枚举方式,面试官都不知道有这种方式

附:完整例子代码+测试代码

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

推荐阅读更多精彩内容