扫码关注微信公众号

回复“面试手册”,获取本站PDF版

回复“简历”,获取高质量简历模板

回复“加群”,加入程序员交流群

回复“电子书”,获取程序员类电子书

当前位置: 计算机基础 > 设计模式 > 单例

手写单例设计模式在面试中是一道非常常见的面试题

先就简单说下单例模式有啥用,为什么要使用单例模式

单例模式保证了一个类只有一个实例,并且提供了一个访问它的全局访问点。

单例模式主要是为了解决一个全局使用的类频繁地创建与销毁。之前介绍JVM的时候有提到Java的内存结构,通过类实例化的对象一般都是放在堆内存中的,频繁的创建对象会使得堆内存不够用,进而触发垃圾回收,这是会影响性能的。(简单解释下这里,这个情况就像你在家里吃零食,垃圾扔的到处都是,你妈进来收拾屋子,肯定会让你先别吃了,然后清理。回到这里,吃零食就是创建对象,所以垃圾清理时JVM中的进程会先停止工作(stop-the-world),反映到用户层面就是系统卡顿了)

单例模式就解决了这个问题,单例模式提供了一个可以反复使用的实例,不必频繁地创建实例。单例模式主要的应用场景如下:

  • 频繁地创建和销毁对象
  • 频繁访问IO资源的对象,例如数据库连接或者访问配置文件
  • 某些对象被创建时会消耗大量的资源,但又经常使用的对象

下面看下单例模式地几种实现方法

饿汉式

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    	return instance;  
    }  
}

通过类加载机制可以知道,类在整个生命周期只会加载一次,类变量也只会被初始化一次,所以在上述类被加载时就会创建一个实例,并且只有一个实例。

优点:在类装载的时候就完成了实例化,也就创建了唯一的实例,避免了线程同步问题

缺点:在类装载时就创建了实例,可能导致资源浪费,因为过早地创建了实例,这个实例可能压根就用不到,最好时是什么时候需要什么时候创建,实现懒加载

懒汉式

public class Singleton {  
    private static Singleton instance;  //和饿汉式不同,这里没有进行实例化
    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  //实例化
        }  
        return instance;  
    }  
}

从上述代码可以看到,懒汉式是在需要实例的时候才进行的实例化,但这样会出现线程安全问题,如果在多线程场景下,一个线程进入了if语句,但未完成实例化,同时另一个线程也到了if (instance == null),因为之前未完成实例化,第二个线程也会进入到if语句,这样就创建了多个实例 优点:实现了懒加载,需要时才创建实例

缺点:多线程场景存在线程安全问题,只能在单线程场景中使用

为了改成线程安全问题,可以考虑加锁

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  //加锁
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

这样做的缺点就是synchronized会影响效率,双重校验锁机制会进一步优化

双重校验锁

加锁的懒汉式,实现了懒加载,但加锁会影响效率,为了解决这个问题,可以在加一层校验,如下

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {                 //第一次
        synchronized (Singleton.class) {  
            if (singleton == null) {        //第二次
                singleton = new Singleton();  
            }  
        }  
    }  
    return singleton;  
    }  
}

相比与加锁的懒汉式,双重校验所机制缩小了锁的范围,大大减少了加锁的次数。先判断是否实例化,如果没有实例化再进行加锁,如果已经实例化就直接返回实例,不再进行加锁。

这时会有小伙伴觉得判断一次singleton是否等于null就可以了,为什么还要判断第二次呢?

因为如果两个线程同时进入了第一个if语句,如果没有第二个if语句,就会先后创建出两个实例,同理如果缺的是第一个if语句,就变成了加锁的懒汉式。

还有一个地方面试时经常会问到,就是修饰singleton的volatile,为什么要用volatile

之前在介绍并发编程时介绍过volatile的作用,可以禁止指令重排序,volatile的实现原理

singleton = new Singleton();这行代码可以分为三步,

  1. 为singleton分配内存空间
  2. 初始化singleton
  3. 将singleton指向分配的内存地址

由于JVM会进行指令重排,简单来说就是,不是按照123顺序执行的,这在单线程情况下不会存在什么问题,但在多线程下就会出现问题

假设有两个线程T1和T2,T1执行到了singleton = new Singleton();,但是是按照132的顺序执行的,刚执行完13步,这时T2执行第一个if语句,发现singleton不等于null,直接return singleton;了,但这时T1还未进行初始化singleton(这里不明白可以看看上面那篇volatile的实现原理)

通过volatile修饰则可以避免指令重排列。

静态内部类

通过静态内部类实现单例是一种比较好的方法,既可以保证线程安全,又能保证懒加载,相比于双重校验锁实现更加简单,代码如下

public class Singleton {  
    private static class Singleton2 {  
    	private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return Singleton2.INSTANCE;  
    }  
}

当Singleton类被加载到内存中时,内部类Singleton2并不会被加载,只有在return Singleton2.INSTANCE;,Singleton2才会被加载,实现了懒加载

并且通过static修饰,可以保证INSTANCE只被初始化一次,避免了多线程的线程安全问题

整个过程没有加锁,代码执行效率也比双重校验锁实现的单例要高

枚举

通过枚举实现单例非常的简单,代码如下

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

虽然通过枚举实现的单例非常简单,但他却是单例的最佳实现方式,因为枚举方式实现的单例不但可以避免线程安全问题还可以避免反射和反序列化对单例模式的破坏

这里简单说下反射和反序列化是如何破坏单例模式的

单例模式主要的特点有以下三点:

  • 实例化的变量引用私有化
  • 构造方法私有化
  • 获取实例的方法公有

这些特点都是为了保证了一个类只有一个实例,并且提供了一个访问它的全局访问点

构造函数私有化的原因很简单,就是为了防止其他类通过 new Singleton()的方式获取实例,而必须调用getInstance(),这才保证了获取的都是同一个实例,但反射可以获取类的构造函数,并通过setAccessible(true)取消 Java言访问检查,可以正常调用私有的构造函数,如下

class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton (){}
    public static Singleton getInstance() {
        return instance;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Singleton singleton = Singleton.getInstance();
        //通过反射创建实例
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton singleton1 = constructor.newInstance();

        System.out.println(singleton.hashCode() == singleton1.hashCode());

    }
}

结果

false

可以看到两个实例不再相同了,反序列化也差不多,就不再重复介绍了

想知道具体原因的可以从反射这块代码点进去看看源码,面试问的不多,就先不写了。

至于面试官让你写手写单例模式时,想多回答点可以写双重校验锁模式,因为这个可供面试官问的知识点多些,还能顺便将话题引到多线程,想写性能好些的可以写静态内部类和枚举类实现的单例


点击面试手册,获取本站面试手册PDF完整版