前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试时被问到单例模式,怎么回答才能让面试官眼前一亮?

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

作者头像
苏三说技术
发布2020-10-15 14:44:25
5800
发布2020-10-15 14:44:25
举报
文章被收录于专栏:苏三说技术苏三说技术

继上一篇《面试前看了这篇spring事务的文章,让我多要了2k的工资》之后,相信朋友们对spring事务的设计原理和底层实现有了更清晰的认识。考虑到马上到来的金九银十的面试季,我给大家介绍一下面试官的必问题,先从单例模式开始,这个设计模式看似简单,想回答得让面试官眼前一亮,还不是那么容易的。

一、什么是单例模式

代码语言:javascript
复制

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

单例模式的特点:
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();

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

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


二、饿汉模式与懒汉模式

什么是饿汉模式?

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

代码语言:javascript
复制
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());
    }
}

什么是懒汉模式?

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

代码语言:javascript
复制
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关键字,改进后的代码如下:

代码语言:javascript
复制
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为空时才加锁,而不为空不应该加锁,需要直接返回。这就需要使用双重检查锁。

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

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

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

三、双重检查锁

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

代码语言:javascript
复制
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是否为空,则可能会重复创建实例。

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

有啥问题?

代码语言:javascript
复制
    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,这样在多线程的情况下同样会创建多次实例。重排之后的代码可能如下:

代码语言:javascript
复制
 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关键字,代码如下:

代码语言:javascript
复制
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 关键字可以保证多个线程的可见性,但是不能保证原子性。同时它也能禁止指令重排。

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

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

四、静态内部类

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

代码语言:javascript
复制
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底层内部机制保证的。

代码语言:javascript
复制
public enum  SimpleSingleton6 {
    INSTANCE;

    public void doSameThing() {

    }
}

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

六、总结

本文主要介绍了:

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

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-08-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 苏三说技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、什么是单例模式
  • 二、饿汉模式与懒汉模式
  • 三、双重检查锁
  • 四、静态内部类
  • 五、枚举
  • 六、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档