Java面试万金油,是单例吖

Java面试万金油,是单例吖

哈啰,各位小伙伴们,这里是每天进步一点点的花栗鼠小K

“前几天叛逆了一下,发生了点小插曲,没有续更设计模式,反而聊起了反射。今天呢,小K接着聊设计模式。之前聊了那么多设计模式,小K后知后觉,发现有个面试中自己经常聊的铁汁被漏掉了。它真的是面试中的万金油,聊啥都能扯到它,设计模式、bean、静态内部类……”

亮个相吧,小宝贝儿。接下来有请潘周,呃, 单例(Singleton)模式



01


什么是单例模式



简言之

单例(Singleton)模式 属于创建型模式,是指一个类只有一个实例,且该类能自行创建这个实例的一种模式

其实生活中有很多地方使用到了单例模式。举个🌰,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。

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

其 UML 如图

Java面试万金油,是单例吖
单例模式UML

单例模式包含两个角色

Singleton:单例类,包含一个实例且能自行创建这个实例的类

Client:访问类,使用单例的类

瞅一眼这个 UML 图,大部分小伙伴都会心一笑,单例模式太TIMI简单了。感觉 Client 作为一个角色都是用来撑场子的,有点子牵强。



02


单例模式实现方式



实现单例模式,有一个通用的模板范式,一般需要满足以下条件

  • 私有化构造方法,避免外部类通过 new 创建对象
  • 定义一个私有的静态变量持有自己的类型
  • 对外提供一个静态的公共方法来获取实例
  • 如果实现了序列化接口需要保证反序列化不会重新创建对象

在此基础上,单例模式有五种实现方式,依次为: 饿汉式 、 懒汉式 、 双重检查锁静态内部类 、 枚举

小K将带领大家,依次揭秘这五种实现方式

1. 饿汉式

饿汉式,顾名思义,类一加载就创建对象,这种方式比较常用,但容易产生垃圾对象,浪费内存空间。

Singleton 代码如下

public class Singleton {
    // 定义私有静态变量持有自己的类型
    private static final Singleton instance = new Singleton();
    // 私有化构造方法
    private Singleton(){}
    // 提供对外静态公共方法
    public static Singleton getInstance() {
        return instance;
    }
}

饿汉式的优缺点如下:

优点:线程安全,没有加锁,执行效率较高

缺点:不是懒加载,类加载时就初始化,浪费内存空间懒汉式

这里提一嘴,懒加载(lazy loading) 指使用时再创建对象的加载方式

饿汉式虽然线程安全,但是可以使用反射破坏该单例

测试代码如下:

public static void main(String[] args) throws Exception {
    Class singletonClass = Class.forName("designPattern.singletonPattern.eager.Singleton");
    Constructor declaredConstructor = singletonClass.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);
    Object o = declaredConstructor.newInstance();

    System.out.println("使用反射创建的饿汉式单例:" + o);
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            System.out.println("多线程创建的饿汉式单例:" + Singleton.getInstance());
        }).start();
    }
}

结果如下:

使用反射创建的饿汉式单例:designPattern.singletonPattern.eager.Singleton@1b6d3586
多线程创建的饿汉式单例:designPattern.singletonPattern.eager.Singleton@6218fb2e
多线程创建的饿汉式单例:designPattern.singletonPattern.eager.Singleton@6218fb2e
多线程创建的饿汉式单例:designPattern.singletonPattern.eager.Singleton@6218fb2e

可见饿汉式单例虽然保证了线程安全,却防止不了反射

2. 懒汉式

懒汉式,在第一次调用时,对其进行实例化

Singleton 代码如下:

public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        // 判断为null 再创建对象
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

最原始的懒汉式单例是线程不安全的

测试如下:

public static void main(String[] args) {
    for (int i=0; i<3; i++){
        new Thread(()->{
            System.out.println("多线程创建的单例模式:" + Singleton.getInstance());
        }).start();
    }
}

结果如下:

多线程创建的单例模式:designPattern.singletonPattern.lazy.Singleton@54437fbd
多线程创建的单例模式:designPattern.singletonPattern.lazy.Singleton@72679c71
多线程创建的单例模式:designPattern.singletonPattern.lazy.Singleton@79a2688b

懒汉式的优缺点如下:

优点:懒加载

缺点:线程不安全

因此在使用时,可以对getInstance() 方法加synchronized关键字

public static synchronized Singleton getInstance() {}

但代价是,每一次调用 getInstance() 获取实例时都需要加锁和释放锁,这样是非常影响性能的。

添加synchronized关键字的懒汉式优缺点如下:

优点:懒加载,线程安全

缺点:效率较低

嗐,果然人生就是,只有失去点什么,才能得到别的东西啊

吐槽结束,进入下一趴

3. 双重检查锁

双重检查锁(即 DCL ,double-checked locking )为了在多线程环境下,不影响程序的性能,不让线程每次调用 getInstance() 方法时都加锁,而只是在实例未被创建时再加锁,在加锁处理里面还需要判断一次实例是否已存在

饿汉式和懒汉式的单例都有缺点,双重检测的实现方式解决了这两者的缺点。

双重检测将懒汉式中的 synchronized 方法改成了 synchronized 代码块。

Singleton代码如下:

public class Singleton {
    private volatile static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        // 第一层检查instance是否为null
        if (instance == null) {
            // synchronized加锁 注意这里是类级别的锁 先判断实例是否存在,若不存在再对类对象进行加锁处理
            synchronized (Singleton.class{
                // 第二层检查instance是否为null 这里的检测避免多线程并发时多次创建对象
                if (instance == null
                    // new关键字创建对象不是原子操作
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

测试代码同懒汉式部分的测试代码,此处不赘述

结果如下:

多线程创建的双重检查锁单例:designPattern.singletonPattern.doubleChecked.Singleton@54437fbd
多线程创建的双重检查锁单例:designPattern.singletonPattern.doubleChecked.Singleton@54437fbd
多线程创建的双重检查锁单例:designPattern.singletonPattern.doubleChecked.Singleton@54437fbd

可见多了些代码,效果是好一些哈

双重检查锁的优缺点如下:

优点:对象的创建是线程安全的,支持延时加载,获取对象时不需要加锁

缺点:实现较复杂

这里细说一下双重检查的两次非空判断哈

  • 第一重判断,如果实例已经存在,就不再需要进行同步操作,而是直接返回这个实例,如果没有创建,才会进入同步块,同步块是为了防止有多个线程同时调用时,导致生成多个实例,有了同步块,每次只能有一个线程调用访问同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创建,之后的所有调用都不会进入同步块,直接在第一重判断就返回了单例
  • 第二重空判断,当多个线程一起到达锁位置时,进行锁竞争,其中一个线程获取锁,如果是第一次进入则为 null,会进行单例对象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返回已创建的单例对象

最关键的一个点就是 volatile 关键字的使用,双重检查锁中使用 volatile 的两个重要特性:可见性 、 禁止指令重排序,此处不再赘述。

4. 静态内部类

用静态内部类的方式实现单例类,利用了Java 静态内部类的特性。Java 加载外部类的时候,不会创建内部类的实例,只有在外部类使用到内部类的时候才会创建内部类实例

Singleton 的代码如下:

public class Singleton {
    private Singleton() {}
    // 定义静态内部类
    private static class InnerClass{
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return InnerClass.INSTANCE;
    }
}

这时候,一个小K前面一直在关注的问题,此时也要重视一下:静态内部类实现方式线程安全吗?

测试代码同懒汉式部分的测试代码,此处不赘述

来看结果:

多线程创建的静态内部类单例:designPattern.singletonPattern.static.Singleton@72679c71
多线程创建的静态内部类单例:designPattern.singletonPattern.static.Singleton@72679c71
多线程创建的静态内部类单例:designPattern.singletonPattern.static.Singleton@72679c71

可见,这一方式也是线程安全的

静态内部类的优缺点如下:

优点:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵),效率较高,实现简单 缺点:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象

5. 枚举

枚举实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性

Singleton代码如下:

public enum Singleton {
    INSTANCE;
}

枚举在Java中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例,可以直接通过调用获取实例

Singleton instance = Singleton.INSTANCE;

枚举方式是线程安全的,且可以避免反射的破坏

测试代码如下:

public static void main(String[] args) throws Exception{
    // 线程安全测试
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            System.out.println("多线程创建的枚举单例:" + designPattern.singletonPattern.static.Singleton.getInstance());
        }).start();
    }
 // 反射测试
    Class singletonClass = Class.forName("designPattern.singletonPattern.enmu.Singleton");
    Constructor declaredConstructor = singletonClass.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);
    Object o = declaredConstructor.newInstance();
    System.out.println("使用反射创建的枚举单例:" + o);
}

结果如下:

多线程创建的枚举单例:INSTANCE
多线程创建的枚举单例:INSTANCE
多线程创建的枚举单例:INSTANCE
Exception in thread "main" java.lang.NoSuchMethodException: designPattern.singletonPattern.enmu.Singleton.<init>()
 at java.lang.Class.getConstructor0(Class.java:3082)
 at java.lang.Class.getDeclaredConstructor(Class.java:2178)
 at designPattern.singletonPattern.enmu.Client.main(Client.java:17)

运行结果报错,所以无法通过反射创建枚举的实例,而枚举类自身特性也保证了线程安全

个人体悟

实现方式 是否线程安全 是否可被反射破坏
饿汉式
懒汉式
双重检查锁
静态内部类
枚举

就我个人而言,一般情况下直接使用饿汉式就好了,如果明确要求要懒加载会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举方式来实现单例



03


单例模式的优缺点



前面说了每种实现方式的优缺点,那么整体看单例模式,其优缺点可以概括如下:

优点:

  • 单例模式可以保证内存里只有一个实例,减少了内存的开销
  • 可以避免对资源的多重占用
  • 单例模式设置全局访问点,可以优化和共享资源的访问

缺点:

  • 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则
  • 并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则



04


总结



本期介绍的单例模式是面试中经常会被问到的一个问题,网上有大量的文章介绍单例模式的实现,本文也是参考那些优秀的文章来做一个总结,通过自己在学习过程中的理解进行记录,并补充完善一些内容,一方面巩固自己所学的内容,另一方面希望能对其他同学提供一些帮助

本期就到这了,这里是花栗鼠小K,下次有🌰,我再来,拜拜~~~

关注六只栗子,面试不迷路!



作者    花栗鼠小K

编辑   一口栗子  


Java面试万金油,是单例吖

Java面试万金油,是单例吖

Java面试万金油,是单例吖

Java面试万金油,是单例吖


原文始发于微信公众号(六只栗子):Java面试万金油,是单例吖

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/88526.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!