优秀的编程知识分享平台

网站首页 > 技术文章 正文

面试时被问到单例模式,怎么回答才能让面试官眼前一亮?

nanyue 2024-09-18 21:47:49 技术文章 5 ℃

考虑到马上到来的金九银十的面试季,我给大家介绍一下面试官的必问题,先从单例模式开始,这个设计模式看似简单,想回答得让面试官眼前一亮,还不是那么容易的。

一、什么是单例模式

单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。该类负责创建自己的对象,同时确保只有一个对象被创建。一般常用在工具类的实现或创建对象需要消耗资源的业务场景。

单例模式的特点

1.类构造器私有

2.持有自己类的引用

3.对外提供获取实例的静态方法


我们先用一个简单示例了解一下单例模式的用法。


public class SimpleSingleton {
    //持有自己类的引用
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    //私有的构造方法
    private SimpleSingleton() {

    }
    //对外提供获取实例的静态方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        System.out.println(SimpleSingleton.getInstance().hashCode());
        System.out.println(SimpleSingleton.getInstance().hashCode());
    }
}

打印结果:

1639705018
1639705018

我们看到两次获取SimpleSingleton实例的hashCode是一样的,说明两次调用获取到的是同一个对象。

可能很多朋友平时工作当中都是这么用的,但是我要说的是这段代码其实是有问题的。

private static final SimpleSingleton INSTANCE = new SimpleSingleton();


一开始就实例化对象了,如果实例化过程非常耗时,并且最后这个对象没有被使用,不是白白造成资源浪费吗?

这个时候你也许会想到,如果在真正使用的时候再实例化不就可以了?这就是我接下来要介绍的 懒汉模式。


二、饿汉模式与懒汉模式

什么是饿汉模式?

实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。好处是没有线程安全的问题,坏处是浪费内存空间。代码如下:


public class SimpleSingleton {
    //持有自己类的引用
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    //私有的构造方法
    private SimpleSingleton() {

    }
    //对外提供获取实例的静态方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        System.out.println(SimpleSingleton.getInstance().hashCode());
        System.out.println(SimpleSingleton.getInstance().hashCode());
    }
}


什么是懒汉模式?

顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。代码如下:


public class SimpleSingleton2 {


    private static SimpleSingleton2 INSTANCE;

    private SimpleSingleton2() {

    }

    public static SimpleSingleton2 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton2();
        }
        return INSTANCE;
    }
    public static void main(String[] args) {
        System.out.println(SimpleSingleton2.getInstance().hashCode());
        System.out.println(SimpleSingleton2.getInstance().hashCode());
    }
}

示例中的INSTANCE对象一开始是空的,在调用getInstance方法才会真正实例化。

如果代码可能有些朋友在使用,但是还是有问题。


有什么问题呢?

假如有多个线程中都调用了getInstance方法,那么都走到 if (INSTANCE == null) 判断时,可能同时成立,因为INSTANCE初始化时默认值是null。这样会导致多个线程中同时创建INSTANCE对象,即INSTANCE对象被创建了多次,违背了一个INSTANCE对象的初衷。


要如何改进呢?

最简单的办法就是使用synchronized关键字,改进后的代码如下:

public class SimpleSingleton3 {

    private static SimpleSingleton3 INSTANCE;

    private SimpleSingleton3() {

    }

    public synchronized static SimpleSingleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton3();
        }
        return INSTANCE;
    }
    public static void main(String[] args) {
        System.out.println(SimpleSingleton3.getInstance().hashCode());
        System.out.println(SimpleSingleton3.getInstance().hashCode());
    }
}


这样总可以了吧?

不好意思,还是有问题。


有什么问题?

使用synchronized关键字会消耗性能,我们应该判断INSTANCE为空时才加锁,而不为空不应该加锁,需要直接返回。这就需要使用双重检查锁。


饿汉模式 和 懒汉模式 各有什么优缺点?

饿汉模式:好处是没有线程安全的问题,坏处是浪费内存空间。

懒汉模式:好处是没有内存空间浪费的问题,但是控制不好实际不是单例。


三、双重检查锁

双重检查锁顾名思义会检查两次,在加锁之前检查一次是否为空,加锁之后再检查一次是否为空。代码如下:

public class SimpleSingleton4 {

    private static SimpleSingleton4 INSTANCE;
    private SimpleSingleton4() {
    }
    public static SimpleSingleton4 getInstance() {        if (INSTANCE == null) {            synchronized (SimpleSingleton4.class) {                if (INSTANCE == null) {                    INSTANCE = new SimpleSingleton4();                }            }        }        return INSTANCE;    }
    public static void main(String[] args) {        System.out.println(SimpleSingleton4.getInstance().hashCode());        System.out.println(SimpleSingleton4.getInstance().hashCode());    }}


在加锁之前判断是否为空,可以确保INSTANCE不为空的情况下,不用加锁,可以直接返回。


为什么在加锁之后,还需要判断INSTANCE是否为空呢?


其实,是为了防止在多线程并发的情况下,比如:线程a 和 线程b同时调用

getInstance,同时判断INSTANCE为空,则同时进行抢锁。假如线程a先抢到锁,开始执行synchronized关键字包含的代码,此时线程b处于等待状态。线程a创建完新实例了,释放锁了,此时线程b拿到锁,进入synchronized关键字包含的代码,如果没有再判断一次INSTANCE是否为空,则可能会重复创建实例。


不要以为这样就完了,还有问题呢?

有啥问题?


    public static SimpleSingleton4 getInstance() {
        if (INSTANCE == null) {//1
            synchronized (SimpleSingleton4.class) {//2
                if (INSTANCE == null) {//3
                    INSTANCE = new SimpleSingleton4();//4
                }
            }
        }
        return INSTANCE;//5
    }

getInstance方法的这段代码,我是按1、2、3、4、5这种顺序写的,希望也按这个顺序执行。但是java虚拟机实际上会有一些优化,对一些代码指令进行重排。重排之后的顺序可能就变成了:1、3、2、4、5,这样在多线程的情况下同样会创建多次实例。重排之后的代码可能如下:


public static SimpleSingleton4 getInstance() {
        if (INSTANCE == null) {//1
           if (INSTANCE == null) {//3
               synchronized (SimpleSingleton4.class) {//2
                    INSTANCE = new SimpleSingleton4();//4
                }
            }
        }
        return INSTANCE;//5
    }
}


原来如此,那有什么办法可以解决呢?


可以在定义INSTANCE是加上volatile关键字,代码如下:


public class SimpleSingleton7 {


    private volatile static SimpleSingleton7 INSTANCE;

    private SimpleSingleton7() {

    }

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

    public static void main(String[] args) {
        System.out.println(SimpleSingleton7.getInstance().hashCode());
        System.out.println(SimpleSingleton7.getInstance().hashCode());
    }
}

volatile 关键字可以保证多个线程的可见性,但是不能保证原子性。同时它也能禁止指令重排。

双重检查锁的机制既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。

除了上面的单例模式之外,还有没有其他的单例模式?


四、静态内部类

静态内部类顾名思义是通过静态的内部类来实现单例模式的。


public class SimpleSingleton5 {

    private SimpleSingleton5() {
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }

    public static void main(String[] args) {
        System.out.println(SimpleSingleton5.getInstance().hashCode());
        System.out.println(SimpleSingleton5.getInstance().hashCode());
    }
}

我们看到在SimpleSingleton5类中定义了一个静态的内部类Inner,SimpleSingleton5类的getInstance方法返回的是内部类Inner的实例INSTANCE。

只有第一次调用getInstance方法时,虚拟机才加载 Inner 并初始化INSTANCE ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。


五、枚举

枚举是天然的单例,每一个实例只有一个对象,这是java底层内部机制保证的。

public enum  SimpleSingleton6 {
    INSTANCE;

    public void doSameThing() {

    }
}

但是实际情况下,枚举的单例用的并不多,因为它不好理解。


六、总结

本文主要介绍了:

饿汉模式、懒汉模式、双重检查锁、静态内部类 和 枚举 这5种单例模式,各有优缺点,静态内部类是所有单例模式中最推荐的模式。


如果您看了这篇文章觉得有所收获,帮忙关注一下我的公众账号:苏三说技术。原创不易,你们的鼓励是我坚持写作最大的动力,谢谢大家。

最近发表
标签列表