面试题:手写单例模式
面试题:手写单例模式
题目分析
单例模式是23种常见设计模式中的一种,也是工作中经常会使用的一种设计模式,在面试中,也经常会要求你手写单例模式。其实面试官想要考察的是,你是否知道单例模式的一些关键的知识点,例如:
- 单例模式的线程安全问题
- 单例模式是否懒加载
我们先来看看单例模式的特征:
- 单例模式的类只能拥有一个对象
- 单例模式的类只能由自己来创建这个唯一的对象
- 单例模式必须要提供方法给外部类获取这个对象
使用单例模式的意图就是保证一个类仅有一个实例,并提供一个访问它的全局访问点。
题目回答
懒汉式
所谓懒汉,就是需要的时候才初始化对象,也就是懒加载。如下代码所示,我们在获取Instance对象时判断对象是否已经创建,如果未创建,才进行创建对象,如果对象已经创建,则直接返回。
懒汉式的问题在于,如果多个线程调用getInstance方法,创建方法是可能被执行多次的,换言之就是线程不安全的。
/**
* 懒汉式
*
* @author freedom wang
* @date 2021-01-12 07:48:33
*/
public class Singleton1 {
private static Singleton1 instance;
private Singleton1() {
}
public static Singleton1 getInstance() {
if (instance == null) {
instance = new Singleton1();
}
return instance;
}
}
线程安全的懒汉式
那既然懒汉式是线程不安全的,能不能加上锁,让它变得线程安全呢。我们对getInstance
方法加上synchronized
关键字,让多线程对于getInstance
方法的调用变成线性调用。这样子就保证了线程安全了。
public class Singleton1 {
private static Singleton1 instance;
private Singleton1() {
}
public static synchronized Singleton1 getInstance() {
if (instance == null) {
instance = new Singleton1();
}
return instance;
}
}
双重校验锁(DCL)
我们使用关键字synchronized创建了一个线程安全的懒汉式单例模式,但是发现没有,整个getInstance方法都被锁住了,即使是后面单例对象已经创建了,其他线程只是去获取单例对象,也要串行获取,这效率有些低,没有必要。
我们能不能只锁住创建单例对象的部分呢,是可以的。
if (instance == null) {
synchronized (Singleton1.class) {
instance = new Singleton1();
}
}
但是还有问题,这样虽然锁住了创建单例对象的部分,但是如果多个线程都判空,还是会创建多个对象,只不过创建的顺序变成的串行而已。
所以我们还需要再加入一次判断,如下所示:
if (instance == null) {
synchronized (Singleton1.class) {
if (instance != null) {
instance = new Singleton1();
}
}
}
还有一个情况,instance = new Singleton1()
这个操作也会被解析成字节码的多步操作,是否会出现,给instance对象赋予一个未初始化的对象,导致其他线程出现未知的异常呢?有文章称在Java2之前是有这个问题的,Java2之后已经不会把一个已经分配空间却没有构造好的对象让其它线程可见。所以保险起见,依旧给instance加一个volatile关键字,防止指令重排。最终的代码是:
public class Singleton1 {
private static volatile Singleton1 instance;
private Singleton1() {
}
public static Singleton1 getInstance() {
if (instance == null) {
synchronized (Singleton1.class) {
if (instance == null) {
instance = new Singleton1();
}
}
}
return instance;
}
}
饿汉式
就是在类加载的时候,就创建单例对象。静态变量的赋值操作是类加载过程的最后一步初始化过程中执行的,也就是在<init>
方法中。
饿汉模式避免了多线程的并发问题,但是也导致了内存的浪费。
代码如下:
public class Singleton2 {
private static Singleton2 instance = new Singleton2();
private Singleton2() {
}
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
枚举
使用双重校验锁和饿汉式创建的单例模式,已经较为完善地完成了单例模式,但是在使用反射的时候,私有化构造器并不保险。可以通过反射的方式创建更多单例对象。第二个问题是序列化问题,单例对象经过序列化和反序列化,返回的单例对象不是同一个。
使用枚举类可以防止反射,也可以让单例对象在序列化和反序列化后返回同一个对象,而且代码非常简单,所以枚举的方式实现单例模式,是一种很好的方式。
代码如下所示:
public enum Singleton3 {
INSTANCE;
public void sayHay(){
}
}