vlambda博客
学习文章列表

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

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

一、什么是单例模式


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

单例模式的特点:

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()); }}

打印结果:
16397050181639705018

我们看到两次获取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类中定义了一个静态的内部类InnerSimpleSingleton5类的getInstance方法返回的是内部类Inner的实例INSTANCE

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


五、枚举

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

public enum SimpleSingleton6 { INSTANCE;
public void doSameThing() {
}}

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


六、总结

本文主要介绍了:

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


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


推荐阅读


  • 揭开 的神秘面纱