通俗易懂理解单例设计模式

1.单例设计模式

1.1 什么是单例设计模式

面试官: 介绍一下单例设计模式

  • 什么是单例设计模式:
    单例就是这个类只有一个实例对象.
  • 单例设计模式解决什么问题:
    一个全局使用的类频繁地创建与销毁. 并且节省内存.
  • 什么时候使用单例设计模式:
    当你想控制类的实例的数目, 节省资源的时候.
  • 单例设计模式的代码特点:
    1. 私有的构造方法;
    2. 私有的静态的成员变量;
    3. 共有的获取实例的静态方法;

1.2 代码实现&各种单例模式的优劣

面试官: 你日常开发中是怎么实现单例的? (面试官可能让你手写一个线程安全的单例)

我一般使用双重判定锁的方式来实现单例.

单例的实现方式有很多种, 比如 饿汉式,懒汉式,双重判定锁 等等方式实现的单例.

  • 饿汉式单例介绍:
    所谓"饿汉", 是指等不及要赶紧创建单例对象. 即在类加载的过程中就进行单例对象的创建. 饿汉就是在类初始化的时候创建单例对象. 具体实现方式有多种.

  • 饿汉式单例的实现代码:

    class Singleton1{
        private Singleton1(){}
        private final static Singleton1 SINGLE = new Singleton1();
        public static Singleton1 getInstance(){
            return SINGLE;
        }
    }
    
  • 饿汉式变种:

    class Singleton {
        private static Singleton instance = null;
        static {
            instance = new Singleton();
        }
        private Singleton() { }
        public static Singleton getInstance() {
            return instance;
        }
    }
    
  • 饿汉式单例优点:
    代码简单; 使用时没有延迟, 在类装载时就完成实例化; 线程安全;

  • 饿汉式单例缺点:
    没有懒加载, 启动比较慢; 无论是否使用这个实例, 这个实例都会创建, 浪费内存;

  • 懒汉式单例介绍:
    所谓"懒汉", 指的是并不会事先初始化出单例对象, 而是在第一次使用的时候再进行初始化.
    懒汉模式有两种写法, 分别是线程安全的和非线程安全的.

  • 懒汉式单例实现代码 – 非线程安全

    class Singleton {
        private static Singleton instance;
        private Singleton() { }
        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    
  • 懒汉式单例实现代码 – 线程安全

    // 线程安全的懒汉式只不过比线程不安全的懒汉式多了一个synchronized关键字
    class Singleton {
        private static Singleton instance;
        private Singleton() { }
        public static synchronized Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    
  • 懒汉式单例优点:
    懒加载, 启动速度快; 只有在使用该类实例的时候才会初始化这个类的实例, 节约资源(内存);

  • 懒汉式单例缺点:

    • 线程不安全的懒汉式缺点:
      • 多线程环境下线程不安全. if (singleton == null) {…} 存在竞态条件, 可能会有多个线程同时进入 if 语句, 导致产生多个实例;
    • 线程安全的懒汉式缺点:
      • 第一次使用的时候需要耗费时间进行对象的初始化.
      • 假如有100个线程同时执行, 那么每次去执行getInstance方法时都要先获得锁再去执行方法体, 如果没有获取锁就要等待, 耗时长, 锁的粒度太大. (效率问题)
  • 双重判定锁实现单例介绍:

    1. 基于懒汉式. 使用同步代码块代替线程安全的懒汉式中的同步方法, 作用是减小锁的粒度, 减少阻塞.
      但是为了保证线程安全需要进行两次非空判断, 所以叫双重判定锁.
    2. 双重if判断: 为了避免了在首层判断就加上Synchorzied同步锁, 导致锁的粒度过大, 引起效率的低下 , 所以采用双重if判断, 在第二层判断才引入对性能开销较大的synchorzied锁, 双重if判断本质上是对效率的优化**(类似读写分离, 读不需要加锁, 写需要加锁)**
    3. volatile: 为了避免JVM的指令重排优化, 为了避免在new对象时出现指令重排序现象, 需要对引用对象用volatile修饰.
      因为初始化对象分为三步:
      ①申请一块内存;
      ②在这块内存里实例化对象;
      ③instance的引用指向这块内存的地址;
      假如先执行1申请一块内存, 再执行3将引用变量指向刚申请的内存, 那么当再执行2的时候, 判断instance是否为空(引用变量是否指向空)的时候就为false, 就不是null, 因此也就不会实例化对象了. 这就是所谓的指令重排序安全问题.
      或者说用下面的说法:
      因为编译器有可能进行指令重排优化, 使得singleton对象再未完成初始化之前就对其进行了赋值, 这样其他人拿到的对象就可能是个残缺的对象了. 使用volatile的目的是避免指令重排. 保证先进行初始化, 然后进行赋值.
  • 双重判定锁实现单例代码:

    class Singleton {
        private Singleton() { }
        private static volatile Singleton instance = null;
        public static Singleton getInstance() {
            if (null == instance) {
                // 同步代码块内还有个if,是为了防止 同时有两个线程到达了这里的情况
                synchronized (Singleton.class) {
                    if (null == instance) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    
        @Test // 测试单例设计模式. 创建50个线程并发调用getInstance()方法并打印返回值. 
        public void test01() {
            ExecutorService threadPool = Executors.newFixedThreadPool(50);
            for (int i = 0; i < 50; i++) {
                threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + "\t" + Singleton.getInstance()));
            }
            threadPool.shutdown();
        }
    
    }
    

  • 其他实现单例的方式 (下面的使用枚举实现单例当做扩展, 面试加分)

  • 使用枚举实现单例的代码:

    public enum Singleton {
        INSTANCE;
        public void whateverMethod() {
        }
    }
    
  • 使用枚举实现单例好处:

    • 写法简单.

    • 枚举其实底层是依赖Enum类实现的, 这个类的成员变量都是static类型的, 并且在静态代码块中实例化的, 和饿汉有点像, 所以他天然是线程安全的.

    • 枚举可以解决反序列化会破坏单例的问题
      在枚举序列化的时候, Java仅仅是将枚举对象的name属性输出到结果中, 反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象. 同时, 编译器是不允许任何对这种序列化机制的定制的, 因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法.

    普通的Java类的反序列化过程中, 会通过反射调用类的默认构造函数来初始化对象. 所以, 即使单例中构造函数是私有的, 也会被反射给破坏掉. 由于反序列化后的对象是重新new出来的, 所以这就破坏了单例.

    但是, 枚举的反序列化并不是通过反射实现的. 所以, 也就不会发生由于反序列化导致的单例破坏问题.

参考自公众号: Hollis

1.3 实际应用

  • Spring框架中单例的应用:
    Spring中的Bean默认是单例的, Spring创建单例Bean使用的是getBean(), 这个方法底层调用的是getSingleton()方法.
    https://www.cnblogs.com/chengxuyuanzhilu/p/6404991.html
    https://blog.csdn.net/hbtj_1216/article/details/74896381
  • 我们自己项目中单例的应用:
    比如我们常见的工具类,连接池类等等就可以定义为单例类.
    https://blog.csdn.net/NathanniuBee/article/details/91872152

1.4 面试思路

如果面试官问
什么是单例设计模式? 你平常怎么使用单例设计模式? 你了解单例设计模式吗…
就按照下面的思路说给他听.

  • 什么是单例设计模式
  • 单例设计模式解决的问题是什么
  • 什么时候使用单例设计模式
  • 常用的框架中哪里使用到了单例设计模式
  • 平常的工作中哪里使用到了单例设计模式
  • 单例设计模式的代码特点是什么
  • 单例设计模式的实现方式有很多种, 比如饿汉式 懒汉式 双重判定锁实现单例等等.
    然后介绍上面三种实现单例的方式的优缺点, 指明自己平常使用的是双重判定锁的单例设计模式,
    面试官有可能会让你写一个线程安全的单例设计模式, 写双重判定锁的即可, 要求该博客中出现的单例设计模式的代码都要会用笔写.
    强调使用双重判定锁的单例的时候成员变量需要加volatile修饰, 说明原因是JVM的指令重排.
    扩展说一下使用枚举来实现单例设计模式的优点.

2. 常见算法(面试默写版)

2.1 冒泡排序

    // 冒泡排序。从小到大排序
    public static void bubbleSort(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - i - 1; j++) {
                // 从大到小排序只需要将下面if中的>改为<即可.
                if (arr[j] > arr[j + 1]) {
                    int tmp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = tmp;
                }
            }
        }
    }

    // 测试冒泡排序
    @Test
    public void test01() throws Exception {
        int arr[] = {3, 4, 2, 1, 5};
        // [3, 4, 2, 1, 5]
        System.out.println(Arrays.toString(arr));
        bubbleSort(arr);
        // [1, 2, 3, 4, 5]
        System.out.println(Arrays.toString(arr));
    }

2.2 二分查找

    // 二分查找
    public static int binarySearch(int[] arr, int value) {
        int low = 0;
        int high = arr.length - 1;
        while (low < high) {
            int mid = (low + high) / 2;
            if (arr[mid] > value) {
                high = mid - 1;
            } else if (arr[mid] < value) {
                low = mid + 1;
            } else {
                return mid;
            }
        }
        return -1;
    }

    // 测试二分查找
    @Test
    public void test01() throws Exception {
        int arr[] = {3, 4, 5, 2, 1, 5};
        // 2 
        System.out.println(binarySearch(arr, 5));
        // -1 
        System.out.println(binarySearch(arr, 10));
    }

2.3 快速排序

    // 快速排序
    public static void quickSort(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }

        int key = arr[left];

        int i = left;
        int j = right;

        while (i < j) {
            while (arr[j] >= key && i < j) {
                j--;
            }
            arr[i] = arr[j];

            while (arr[i] <= key && i < j) {
                i++;
            }
            arr[j] = arr[i];
        }

        arr[j] = key;

        quickSort(arr, left, i - 1);
        quickSort(arr, i + 1, right);
    }

    // 测试快速排序
    @Test
    public void test01() throws Exception {
        int[] arr = {3, 1, 2, 5, 4};
        quickSort(arr, 0, arr.length - 1);
        // [1, 2, 3, 4, 5] 
        System.out.println(Arrays.toString(arr));
    }

2.4 递归求阶乘

    // 递归求阶乘
    public static int multiply(int num) {
        if (num < 0) {
            System.out.println("请输入大于0的整数");
            return -1;
        } else if (num == 0 || num == 1) {
            return 1;
        } else {
            return multiply(num - 1) * num;
        }
    }

    // 测试递归求阶乘
    @Test
    public void test01() throws Exception{
        // 6 
        System.out.println(multiply(3));
    }

2.5 线程安全的单例模式

    // 线程安全的单利设计模式。 双重判定锁
    class Singleton {
        private Singleton() {
        }

        private static volatile Singleton instance = null;

        public static Singleton getInstance() {
            if (null == instance) {
                synchronized (Singleton.class) {
                    if (null == instance) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

    // 测试单例设计模式. 创建50个线程并发调用getInstance()方法并打印返回值.
    @Test
    public void test01() throws Exception {
        final ExecutorService threadPool = Executors.newFixedThreadPool(50);
        for (int i = 0; i < 50; i++) {
            threadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "\t" + Singleton.getInstance());
            });
        }
        threadPool.shutdown();
    }

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值