单例模式(Singleton)


单例模式(Singleton).

定义:.

指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。

在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。

单例模式有 3 个特点.

  1. 单例类只有一个实例对象;(私有构造器)
  2. 该单例对象必须由单例类自行创建;(内部new)
  3. 单例类对外提供一个访问该单例的全局访问点;(getInstance静态方法)

单例的实现.

  • 实现方式

    • 饿汉式(类加载时初始化,线程安全,如果未获取实例,会造成资源浪费)
    • 懒汉式(类加载时不初始化,获取实例时初始化,线程不安全,不会造成资源浪费)
    • 懒汉式(线程安全版)
      • synchronized getInstance()(synchronized是非常消耗性能,多次调用该方法会造成不必要的性能消耗)
      • 双重校验锁(DCL)(解决了多次调用getInstance对性能消耗的影响,但是存在指令重排问题)
      • 双重校验锁(DCL)+ volatile (解决指令重排问题,但是反射可以破解单例模式)
      • 三重校验锁(在构造器中再加一个验证,仍然有缺陷,只适用于先产生单例对象,再使用反射获取对象 的情况)
      • 红绿灯(可以简单解决放射问题,但是仍然有问题)
    • 静态内部类(类加载时不初始化)
    • 枚举(默认单例,不能通过反射破解)

    除了枚举所有的实现方式,在反射机制面前,都是不安全

1.饿汉式.

饿汉式 线程安全

public class Hungry {
    //私有构造器
    private Hungry(){
        System.out.println(Thread.currentThread().getName());
    }
    //内部 new对象 private final static
    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }

    /*
        饿汉式 由于在内部已经new 实力化
        会造成资源浪费
     */
    //多线程并发
    public static void main(String[] args) {
        for (int i = 0; i < 10 ; i++) {
            // lambda表达式 jdk8
            new Thread(()->{
                Hungry.getInstance();
            }).start();
        }

        /*
            结果发现 始终只有一个实例产生
            (没有考虑到反射情况下)
        */
    }
}

结果显示

2.懒汉式(线程不安全).

懒汉式(普通)存在多线程问题

public class LazyMan {
    //私有构造器
    private LazyMan(){
        System.out.println(Thread.currentThread().getName());
    }
    //声明域对象 但是不实例化 到要使用时 实例化 private static  相比于饿汉式 没有 final
    private  static  LazyMan LAZYMAN;
    //获取实例
    public static LazyMan getInstance(){
        if (LAZYMAN == null){
            LAZYMAN = new LazyMan();
        }
        return LAZYMAN;
    }
    /*
        虽然解决了 内存浪费问题,在单线程下也是OK的, 对象是唯一的
        但是:多线程下,是不安全的,对象可能 不是唯一

        看main方法中的测试可以看出
        多次的结果产生了多个对象

        我们需要的是单例

        因此我们需要同步
     */
    //多线程并发
    public static void main(String[] args) {
        for (int i = 0; i < 10 ; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }
}

结果演示

3.懒汉式(线程安全).

懒汉式(synchronized getInstance())为了解决多线程问题

public class LazyMan_Synr {
    //私有构造器
    private LazyMan_Synr(){
        System.out.println(Thread.currentThread().getName());
    }
    //声明域对象 但是不实例化 到要使用时 实例化 private static  相比于饿汉式 没有 final
    private  static LazyMan_Synr LAZYMAN;
    //获取实例 同步操作
    public static synchronized LazyMan_Synr getInstance(){
        if (LAZYMAN == null){
            LAZYMAN = new LazyMan_Synr();
        }
        return LAZYMAN;
    }
    /*
        虽然进行了同步处理,解决了多线程问题

        但是synchronized操作是非常消耗性能的

        我们可以使用 DCL 过滤不必要的 同步代码块的调用
     */
    //多线程并发
    public static void main(String[] args) {
        for (int i = 0; i < 10 ; i++) {
            new Thread(()->{
                LazyMan_Synr.getInstance();
            }).start();
        }
    }
}

结果显示

双重校验锁(DCL)为了解决多线程问题

DCL单例模式为什么要两次判空.

来自简书(next_discover)

  • getInstance方法中第一个判空条件,逻辑上是可以去除的,去除之后并不影响单例的正确性,但是去除之后效率低。因为去掉之后,不管instance是否已经初始化,都会进行synchronized操作,而synchronized是一个重操作消耗性能。加上之后,如果已经初始化直接返回结果,不会进行synchronized操作。
  • getInstance方法中的第二个判空条件是不可以去除,如果去除了,并且刚好有两个线程a和b都通过了第一个判空条件。此时假设a先获得锁,进入synchronized的代码块,初始化instance,a释放锁。接着b获得锁,进入synchronized的代码块,也直接初始化instance,instance被初始化多遍不符合单例模式的要求~。加上第二个判空条件之后,b获得锁进入synchronized的代码块,此时instance不为空,不执行初始化操作。
public class LazyMan_Lock {
    //私有构造器
    private LazyMan_Lock(){
        System.out.println(Thread.currentThread().getName());
    }
    //声明域对象 但是不实例化 到要使用时 实例化 private static  相比于饿汉式 没有 final
    private  static LazyMan_Lock LAZYMAN;

    //获取实例 双重检查锁模式的 懒汉式单例 (DCL懒汉式)
    public static LazyMan_Lock getInstance(){
        if (LAZYMAN == null){//外层检测
            synchronized (LazyMan_Lock.class){//同步锁
                if (LAZYMAN == null){//内层检测
                    LAZYMAN = new LazyMan_Lock(); // 不是一个原子性操作
                    /*
                        new一个对象 正常的3步实现
                        1.分配内存空间
                        2.执行构造方法,初始化对象
                        3.把这个对象指向这个空间
                     */
                }
            }
        }
        return LAZYMAN;
    }
    /*
        DCL懒汉式 
        降低了 synchronized 对性能的影响

        获得的结果只有个一个实例

        但是仍然存在问题
        由于上面
        LAZYMAN = new LazyMan_Lock(); // 不是一个原子性操作

        new 实例的顺序就有可能被打破

        假如A获取实例 构造的顺序 是 132
            1.分配内存空间
            3.把这个对象指向这个空间
            2.执行构造方法,初始化对象
        此时B又获取实例的话,由于LAZYMAN != null,他就会认为实例是存在的,紧接着返回这个实例,
        实际上这个实例指向的是一片虚无,就会报错

     */

    //多线程并发
    public static void main(String[] args) {
        for (int i = 0; i < 10 ; i++) {
            new Thread(()->{
                LazyMan_Synr.getInstance();
            }).start();
        }
    }
}

展示结果

  • 结果与上一个无区别

  • 存在指令重排问题(目前无法展示出结果,发生概率太小)

.

指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。

指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。

.

懒汉式(双重校验锁(DCL)+ volatile)为了解决多线程问题 + 原子性

public class LazyMan_Lock_Atom {
    //私有构造器
    private LazyMan_Lock_Atom(){
        System.out.println(Thread.currentThread().getName());
    }

    //声明域对象 但是不实例化 到要使用时 实例化 private static  相比于饿汉式 没有 final
    private volatile static LazyMan_Lock_Atom LAZYMAN;

    //获取实例 双重检查锁模式的 懒汉式单例 (DCL懒汉式)
    public static LazyMan_Lock_Atom getInstance(){
        if (LAZYMAN == null){//外层检测
            synchronized (LazyMan_Lock_Atom.class){//同步锁
                if (LAZYMAN == null){//内层检测
                    LAZYMAN = new LazyMan_Lock_Atom(); // 不是一个原子性操作
                }
            }
        }
        return LAZYMAN;
    }
    /*
        增加volatile修饰符 保证原子性
        private volatile static LazyMan_Lock_Atom LAZYMAN;

        当时这些仍然是有问题的
        由于放射可以获得 该类的私有构造器,所以可以轻易破解 单例模式 获取多个实例
     */

    //使用反射破解 单例模式
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//        for (int i = 0; i < 10 ; i++) {
//            new Thread(()->{
//                LazyMan_Lock_Atom.getInstance();
//            }).start();
//        }
        //单例模式创建的实例
        LazyMan_Lock_Atom LazyMan = LazyMan_Lock_Atom.getInstance();
        //反射创建的实例
        Constructor<LazyMan_Lock_Atom> constructor = LazyMan_Lock_Atom.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazyMan_Lock_Atom LazyMan1 = constructor.newInstance();

        System.out.println("单例模式创建的实例 "+ LazyMan);
        System.out.println("反射创建的实例 "+ LazyMan1);
        /*
            运行可以看到 两次产生对象不是同一个
         */

    }
}

结果演示

  • 结果是一致
  • 这里我们考虑反射破解(运行可以看到 两次产生对象不是同一个,单例被破解)

懒汉式(三重校验锁)为了解决反射破解问题

public class LazyMan_Lock_Atom_PRO {
    //私有构造器
    private LazyMan_Lock_Atom_PRO(){
        System.out.println(Thread.currentThread().getName());
        synchronized (LazyMan_Lock_Atom_PRO.class){//第三重检测
            if (LAZYMAN != null){//如果对象(单例模式创建的)存在
                //抛出异常 由于 反射破解 私有构造器
                throw new RuntimeException("不要试图使用反射破解异常");
            }
        }
    }

    //声明域对象 但是不实例化 到要使用时 实例化 private static  相比于饿汉式 没有 final
    private volatile static LazyMan_Lock_Atom_PRO LAZYMAN;

    //获取实例 双重检查锁模式的 懒汉式单例 (DCL懒汉式)
    public static LazyMan_Lock_Atom_PRO getInstance(){
        if (LAZYMAN == null){//外层检测
            synchronized (LazyMan_Lock_Atom_PRO.class){//同步锁
                if (LAZYMAN == null){//内层检测
                    LAZYMAN = new LazyMan_Lock_Atom_PRO(); // 不是一个原子性操作
                }
            }
        }
        return LAZYMAN;
    }
    /*
        在私有构造器中添加第三重检测

        看上去好像 是解决了反射破解构造器的问题
        事实上,仍然有问题
            这个只适用于 先产生单例对象,再使用反射获取对象 的情况
        如果没有 产生使用单例模式创建对象 而是 使用 反射

     */

    //使用反射破解 单例模式
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        //反射创建的实例
        Constructor<LazyMan_Lock_Atom_PRO> constructor = LazyMan_Lock_Atom_PRO.class.getDeclaredConstructor();
        constructor.setAccessible(true);

        //************************ 三重检测结果 ***************************//
        /*
        //单例模式创建的实例
        LazyMan_Lock_Atom_PRO LazyMan = LazyMan_Lock_Atom_PRO.getInstance();
         //反射实例1
        LazyMan_Lock_Atom_PRO LazyMan1 = constructor.newInstance();

        System.out.println("单例模式创建的实例 "+ LazyMan);
        System.out.println("反射创建的实例1 "+ LazyMan1);
        */
        //************* 仍然存在的问题(先注释“三重检测结果”部分)*************//
        //反射实例1
        LazyMan_Lock_Atom_PRO LazyMan1 = constructor.newInstance();
        //反射实例2
        LazyMan_Lock_Atom_PRO LazyMan2 = constructor.newInstance();

        System.out.println("反射创建的实例1 "+ LazyMan1);
        System.out.println("反射创建的实例2 "+ LazyMan2);
    }
}

结果展示

  • 三重检测结果

  • 仍然存在的问题(没有使用getInstance获取实例,只使用反射获取实例,仍然可以破解)

懒汉式(红绿灯)为了解决反射破解问题

public class LazyMan_Lock_Atom_PRO_Signals {

    //添加红绿灯
    private static boolean qwert = false;

    //私有构造器
    private LazyMan_Lock_Atom_PRO_Signals(){
        System.out.println(Thread.currentThread().getName());

        if (qwert == false){//如果实例 不存在
            qwert = true;
        }else{//如果实例存在
            //抛出异常 由于 反射破解 私有构造器
            throw new RuntimeException("不要试图使用反射破解异常");
        }

    }

    //声明域对象 但是不实例化 到要使用时 实例化 private static  相比于饿汉式 没有 final
    private volatile static LazyMan_Lock_Atom_PRO_Signals LAZYMAN;

    //获取实例 双重检查锁模式的 懒汉式单例 (DCL懒汉式)
    public static LazyMan_Lock_Atom_PRO_Signals getInstance(){
        if (LAZYMAN == null){//外层检测
            synchronized (LazyMan_Lock_Atom_PRO_Signals.class){//同步锁
                if (LAZYMAN == null){//内层检测
                    LAZYMAN = new LazyMan_Lock_Atom_PRO_Signals(); // 不是一个原子性操作
                }
            }
        }
        return LAZYMAN;
    }
    /*
        使用 红绿灯(一个标志位) 解决 反射破解 构造器 创建对象
            私有一个boolean static(保持唯一) 属性 由于这个属性 正常角度是无法通过放射去获取这个 属性的(因为你不知道属性名是啥) 并更改这个属性值的 所以一般来说已经很安全
            但是 如果 是在 反编译的情况下 同样可以获取 该属性的名字 然后更改 属性值 破解构造器 创建实例

            下面的就不做演示,也就是说 道高一尺,魔高一丈

     */

    //使用反射破解 单例模式
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        //反射创建的实例
        Constructor<LazyMan_Lock_Atom_PRO_Signals> constructor = LazyMan_Lock_Atom_PRO_Signals.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        //反射实例1
        LazyMan_Lock_Atom_PRO_Signals LazyMan1 = constructor.newInstance();
        //反射实例2
        LazyMan_Lock_Atom_PRO_Signals LazyMan2 = constructor.newInstance();

        System.out.println("反射创建的实例1 "+ LazyMan1);
        System.out.println("反射创建的实例2 "+ LazyMan2);
    }
}

结果展示

4.静态内部类.

public class Outer {
    private Outer(){
    }
    private static class Inner{
        private static final Outer instance = new Outer();
    }
    // 调用该方法,获取实例,并进行初始化
    public static Outer getInstance(){
        return Inner.instance;
    }
}

5.枚举.

//enum 默认就是一个单列的 可以去看 Constructor.class 中 newInstance()
    /*
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects"); // 不能反射地创建枚举对象
     */
public enum EnumSingle {
    INSTANCE;

    private EnumSingle(){

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

class Test{
    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
//        EnumSingle instance1 = EnumSingle.getInstance();
//        EnumSingle instance2 = EnumSingle.getInstance();
//
//        System.out.println(instance1);
//        System.out.println(instance2);

        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor();
        constructor.setAccessible(true);

        EnumSingle enumSingle1 = constructor.newInstance();
        EnumSingle enumSingle2 = constructor.newInstance();

        System.out.println(enumSingle1);
        System.out.println(enumSingle2);

        //反编译 1
        //javap -p **.class

        //反编译 2 专业
        //jad.exe 将 class反编译为 java文件
        //jad -sjava **.class
    }
}

文章作者: liuminkai
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 liuminkai !
评论
 上一篇
抽象工厂(AbstractFactory)模式 抽象工厂(AbstractFactory)模式
抽象工厂(AbstractFactory)模式.一、工厂模式. 作用: 实现了创建者和调用者的分离 详细分类 简单工厂模式 用来生产同一等级结构中的任一产品(对于增加新的产品,需要求修改已有代码) 工厂方法模式 用来生产同一等级
2020-07-18
下一篇 
javase -- 变量 javase -- 变量
变量. 内存中的一个存储区域 该区域的数据可以在同一类型范围内不断变化 变量是程序中最基本的存储单元 变量类型 变量名 = 存储的值; 作用 用于在内存中保存数据 注意 先声明,后使用 变量只在其作用域中有效 同一个作用域中不能声明
2020-07-18
  目录