单例模式是23种GOF模式中最简单,也是最经常出现的一种设计模式,也是面试官最常爱考的一种模式,为什么呢? 因为单例模式足够简单,编写一个单例模式代码几分钟就能搞定,所以设计模式中面试官通常会选取单例模式作为出题。 下面把单例模式分几个点,分别说说哪些地方面试官能考你?
单例模式的意义
通常面试官会很笼统的问你,什么是单例模式?单例模式用来解决了什么痛点?没有单例模式我们会怎么办?单例模式他有什么缺点吗?
单例模式是最简单的设计模式之一,属于创建型模式,它提供了一种创建对象的方式,确保只有单个对象被创建。这个设计模式主要目的是想在整个系统中只能出现类的一个实例,即一个类只有一个对象。 单例模式的解决的痛点就是节约资源,节省时间从两个方面看:
1.由于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级的对象而言,是很重要的.
2.因为不需要频繁创建对象,我们的GC压力也减轻了,而在GC中会有STW(stop the world),从这一方面也节约了GC的时间 单例模式的缺点:简单的单例模式设计开发都比较简单,但是复杂的单例模式需要考虑线程安全等并发问题,引入了部分复杂度。
扩展:从你的回答中能进行哪些扩展呢?我们谈到了GC,有可能这时候就会问你GC,STW等知识。谈缺点的时候谈到了复杂的单例模式, 这个时候可能会问你让你设计一个优秀的单例模式你会怎么设计,会怎么实现?
单例模式的设计
通常这里面试官会问你单例模式怎么设计,需要看重哪些方面?一般来说单例模式有哪些实现方式?
设计单例模式的时候一般需要考虑几种因素:
-线程安全 -延迟加载 -代码安全:如防止序列化攻击,防止反射攻击(防止反射进行私有方法调用) -性能因素
一般来说,我们去网上百度去搜大概有7,8种实现,,下面列举一下需要重点知道的 饿汉,懒汉(线程安全,线程非安全),双重检查(DCL)(重点),内部类,以及枚举(重点), 下面比对下各个实现:
| 线程安全 | 并发性能好 | 可以延迟加载 | 序列化/反序列化安全 | 能抵御反射攻击 | |
---|---|---|---|---|---|---|
饿汉式 | Y | Y |
|
|
| |
懒汉式 | 不加锁 |
| Y | Y |
|
|
加锁的 | Y |
| Y |
|
| |
DCL | Y | Y | Y |
|
| |
静态内部类 | Y | Y | Y |
|
| |
枚举 | Y | Y |
| Y | Y |
扩展:我们上面说到了各个模式的实现,这个时候很有可能会叫你手写各个模式的代码。当然也有可能会问你线程安全,代码安全等知识。
饿汉模式
饿汉模式的代码如下:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
饿汉模式代码比较简单,对象在类中被定义为private static,通过getInstance(),通过java的classLoader机制保证了单例对象唯一。 扩展:
有可能会问instance什么时候被初始化?
Singleton类被加载的时候就会被初始化,java虚拟机规范虽然没有强制性约束在什么时候开始类加载过程,但是对于类的初始化,虚拟机规范则严格规定了有且只有四种情况必须立即对类进行初始化,遇到new、getStatic、putStatic或invokeStatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。 生成这4条指令最常见的java代码场景是:1)使用new关键字实例化对象2)读取一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)3)设置一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)4)调用一个类的静态方法
class的生命周期?
class的生命周期一般来说会经历加载、连接、初始化、使用、和卸载五个阶段
class的加载机制
这里可以聊下classloader的双亲委派模型。
双重检查DCL
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;
}
}
synchronized同步块里面能够保证只创建一个对象。但是通过在synchronized的外面增加一层判断,就可以在对象一经创建以后,不再进入synchronized同步块。这种方案不仅减小了锁的粒度,保证了线程安全,性能方面也得到了大幅提升。
同时这里要注意一定要说volatile,这个很关键,volatile一般用于多线程的可见性,但是这里是用来防止指令重排序的。
扩展:
为什么需要volatile?volatile有什么用?
- 首先要回答可见性,这个是毋庸质疑的,然后可能又会考到java内存模型。
- 防止指令重排序: 防止new Singleton时指令重排序导致其他线程获取到未初始化完的对象。instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。1.给 instance 分配内存2.调用 Singleton 的构造函数来初始化成员变量3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了) 但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后报错。
- 顺便也可以说下volatie原理用内存屏障
讲讲synchronized和volatile的区别
这里可以从synchroized能保证原子性,volatile不能保证说起,以及讲下synchroized是重量级锁,甚至可以所以下他和Lock的区别等等。
线程安全一般怎么实现的?
- 互斥同步。如lock,synchroized
- 非阻塞同步。如cas。
- 不同步。如threadLocal,局部变量。
枚举类
public enum Singleton{
INSTANCE;
}
默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。同时他也是《Effective Java》中推荐的模式。最后通过枚举类,他能自动避免序列化/反序列化攻击,以及反射攻击(枚举类不能通过反射生成)。
总结
单例模式虽然看起来简单,但是设计的Java基础知识非常多,如static修饰符、synchronized修饰符、volatile修饰符、enum等。这里的每一个知识点都可以变成面试官下手的考点,而单例只是作为一个引子,考到最后看你到底掌握了多少。看你的广度和深度到底是怎么样的。
最后这篇文章被我收录于JGrowing,一个全面,优秀,由社区一起共建的Java学习路线,如果您想参与开源项目的维护,可以一起共建,github地址为:github.com/javagrowing… 麻烦给个小星星哟。
如果大家觉得这篇文章对你有帮助,或者想提前获取后续章节文章,或者你有什么疑问想提供1v1免费vip服务,都可以关注我的公众号你的关注和转发是对我最大的支持,O(∩_∩)O: