看了这篇文章,你能和面试官畅谈单例模式

Vidonia ·
更新时间:2024-11-13
· 972 次阅读

看了这篇文章,你能和面试官畅谈单例模式 一、前言

最近看了很多的书还有视频,他们都花了很长的篇幅提到了单例模式,于是我想把他们都总结起来,写下这篇文章。目的就是,让小白能搞懂单例模式,以及单例模式的经典面试题。为什么说是小白也能懂的呢?哈哈哈,还不是小胖也是一个小白~~~

二、单例模式的解释

单例模式定义:一个类只能有一个实例,且该类能自行创建这个实例的一种模式。其实单例模式在C#或者.NET里面更好理解,像win7的任务管理器,在系统中只能创建一个。有些理解了嘛?

单例模式只能有一个实例,实例化其实就是new的过程,是不可能阻止他人不去用new的。所以我们完全可以直接就把这个类的构造方法改成私有的。对于外部的代码,不能用new来实例化他,我们完全可以再写一个public方法,叫做getInstance(),这个方法的目的就是返回一个实例,但是在这个方法中,我们需要是否实例化的判断。

来一个简单的例子 package singleton; /** * 描述: 懒汉式(线程不安全) * **/ public class Singleton3 { private static Singleton3 instance; private Singleton3(){ } //如果两个线程同时到达,会出现线程不安全的情况 public static Singleton3 getInstance(){ if (instance == null){ instance = new Singleton3(); } return instance; } }

单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。可以节省内存和计算、保证结果正确、方便管理。适用场景是无状态的工具类、全局信息类。

记住,上面的单例模式是线程不安全的。

三、实现单例模式的8种写法 1.饿汉式(静态常量)(可用) package singleton; /*** * 描述:饿汉式(静态常量) (可用) * **/ public class Singleton1 { private final static Singleton1 INSTANCE = new Singleton1(); private Singleton1(){ } public static Singleton1 getInstance(){ return INSTANCE; } }

上面是饿汉式的静态常量的写法,可以看到类在加载后就完成了实例化的创建。优点:写法简单,类加载后就完成了实例的创建。缺点:提前占用系统的资源。

2.饿汉式(静态代码块)(可用) package singleton; /*** * 描述:饿汉式(静态代码块) (可用) * **/ public class Singleton2 { private final static Singleton2 INSTANCE; static { INSTANCE = new Singleton2(); } private Singleton2(){ } public static Singleton2 getInstance(){ return INSTANCE; } }

上面是饿汉式的静态代码块的方式,只不过和第一种有一些区别。优缺点和第一种是一样的。优点:写法简单,类加载后就完成了实例的创建。缺点:提前占用系统的资源。

3.懒汉式(线程不安全)(不可用) package singleton; /** * 描述: 懒汉式(线程不安全) * **/ public class Singleton3 { private static Singleton3 instance; private Singleton3(){ } //如果两个线程同时到达,会出现线程不安全的情况 public static Singleton3 getInstance(){ if (instance == null){ instance = new Singleton3(); } return instance; } }

上面的懒汉式的写法,它是线程不安全的,因为在多线程的时候,可能会有两个线程同时到达instance==null,然后都会初始化,这就创建了两个对象了,是线程不安全的,不能使用。

4.懒汉式(线程安全)(不推荐) package singleton; /** * 描述:懒汉式(线程安全)(不推荐) * */ public class Singleton4 { private static Singleton4 instance; private Singleton4(){ } //但是效率不高 public synchronized static Singleton4 getInstance(){ if (instance == null){ instance = new Singleton4(); } return instance; } }

上面是懒汉式的第二种写法,这种方法是线程安全的,但是不推荐使用,因为效率是低下的。在getInstance上面加上了synchronized的同步方法,那么只能有一个线程可以进入到这个方法中,但是在多线程的时候效率是非常低的,因为任何一个线程进入的这个方法,都需要去等待锁的释放,所以不推荐使用。

5.懒汉式(线程不安全)(不可用) package singleton; /** * 描述:懒汉式(线程不安全) (不推荐) * */ public class Singleton5 { private static Singleton5 instance; private Singleton5(){ } public static Singleton5 getInstance(){ if (instance == null){ synchronized (Singleton5.class){ instance = new Singleton5(); } } return instance; } }

上面是懒汉式的第三种写法,是对第二种的改进,不在锁在方法上,加在创建对象上面,但是这可能会引发线程安全的问题。如果连个线程都判断instance==null,都进入到if里面,这时只有一个线程会运行,但是第一个线程执行完成之后,第二个线程还是会创建实例,那么就是线程不安全的。

6.懒汉式(双重检查)(推荐面试使用) package singleton; /** * 描述: 双重检查 * 优点: 线程安全;延迟加载;效率较高 * 为什么要double-check * 1.线程安全 * 2.单check为什么不行? * 3.放在判断后面会引发线程安全问题 * 4.单层锁,但是synchronized放在方法上,这样可以,但是会导致性能问题 * * 为什么要用volatile * 1.新建对象实际上有3个步骤(分配内存资源,调用构造函数,将对象指向分配的内存空间)新建对象不是原子操作。 * 2.JVM重排序会带来NPE(空指针的问题) * 3.防止重排序 * */ public class Singleton6 { private volatile static Singleton6 instance; private Singleton6(){ } public static Singleton6 getInstance(){ if (instance == null){ synchronized (Singleton6.class){ if (instance == null){ instance = new Singleton6(); } } } return instance; } }

上面就是很有名的double-check单例模式了,它的优点有:线程安全;延迟加载;效率较高。它是线程安全的,而且效率很高,推荐我们在面试的时候使用。这个代码同时也引出了我们在面试过程中的2个问题。
懒汉式单例模式为什么要用double-check,不用就不安全吗?懒汉式单例模式为什么双重检查模式要用volatile?

7.懒汉(静态内部类方式)(可用) package singleton; /** * 描述: 静态内部类方式,可用 * 懒汉 * ***/ public class Singleton7 { private Singleton7(){ } private static class SingletonInstance{ private static final Singleton7 INSTANCE = new Singleton7(); } public static Singleton7 getInstance(){ return SingletonInstance.INSTANCE; } }

上面是静态内部类的方式,是可以推荐使用的,而且效率也是可以的。外部类加载,JVM不会创建多个实例。

8.枚举 推荐在项目中使用 package singleton; /** * 描述:单例模式:枚举 推荐用 * */ public enum Singleton8 { INSTANCE; public void whatever(){ //无论什么方法 } }

上面是枚举方式的单例模式,是生产实践中最佳的单例模式的写法,同时可以防止反序列化破坏单例。

四、常见面试问题 什么叫单例设计模式

答:单例模式的重点在于整个系统上共享一些创建时较耗资源的对象。整个应用只维护一个特定类实例,它被所有组件共同使用。Java.lang.Runtime是单例模式的经典例子。

你知道饿汉式的缺点吗?

答:饿汉模式,类一加载的时候就会实例化对象,所以要提前占用系统资源。

那懒汉式的缺点呢?

答:不会出现占用资源的问题,但是需要使用合适,否则会带来线程安全问题。

懒汉式单例模式为什么要用double-check,不用就不安全吗?

答:为了线程安全,我们需要使用double-check。

追问,单check为什么不行?(代码见5.懒汉式)

答:单check是线程不安全的(代码见5.懒汉式),可能会有多个线程走到了 if (instance == null)的里面,由于synchronized看似只能有一个线程会创建对象,但是第二个也会创建。

追问,你可以把synchronized写在方法的外面呀?(代码见4,。懒汉式)

答:这个是可以解决线程安全的问题,但是效率不是很高,每个线程都需要等待锁的释放,会导致性能的问题,不推荐使用。

你说为什么要用volatile?

答:在多线程的时候,创建对象分为3步,CPU可能会重排序,首先建一个空的对象,然后复制给引用,然后调用构造方法。
第一个线程进来了,第一个对象已经不是空的,但是构造方法没有执行,里面的属性是空的。第二个线程发现不是空的,就会直接跳过创建实例的方法,之后再使用的时候引发的问题。使用volatile可以避免这个问题,对于第二个线程来说,他的创建过程对第一个线程来说是可见了,他就会等待创建完成。

那这么多应该如何选择,用哪种单例的实现方案最好?

答:枚举方式的单例模式,是生产实践中最佳的单例模式的写法,同时可以防止反序列化破坏单例。

那你知道happens-before原则嘛?

答:volatile就是happens-before原则呀…未完待续

请用Java写出线程安全的单例模式。

答:上面已经很多例子了,小胖觉得可以用第6种double-check。

五、关于几种解法的选择

《剑指offer》上面的推荐给面试官的解法是1.饿汉式(静态常量)(可用)和7.懒汉(静态内部类方式)(可用)。
《大话设计模式》上面的推荐也是1.饿汉式(静态常量)(可用)
《线程八大核心+Java并发底层原理精讲》上面推荐使用6.懒汉式(双重检查)(推荐面试使用)

小胖觉得可以使用第6种,这可能会打开一些面试的问题,把问题引入到我们了解熟悉的方向。当然你在手写单例模式的时候,可以去询问一下要求,是需要饿汉式还是懒汉式。

六、参考资料

书籍1:《大话设计模式》 第21章 有些类也需计划生育–单例模式
书籍2:《剑指offer》 面试题2:实现Singleton模式
视频:《线程八大核心+Java并发底层原理精讲》

七、关于本系列的解释

本系列想制作23种设计模式+7种设计原则一系列课程,其目的就是一个简单的记录学习的过程。不知道能帮助到多少人,也不知道技术是否会有一定的深度。

制作不易,您的点赞是我最大的动力。点赞10个,我会发出下一篇文章


作者:小胖丨学编程



面试 面试官 单例模式

需要 登录 后方可回复, 如果你还没有账号请 注册新账号