非常常规的一道面试题,UP在字节实习二面的时候被问到了这个问题(话说字节真的非常看重算法能力,每一面都会至少手撕两道算法题目)。
接下来给出最常见的几种单例模式的实现
饿汉式—静态常量方式:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
在类加载的时候就初始化了实例对象,因此在多线程的情况下是线程安全的。
饿汉式—静态代码块方式
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
将类的实例化放在了静态代码块中,保证只会初始化一次,同样线程安全。
懒汉式(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
多线程场景下禁止使用,因为可能会产生多个对象,线程不安全。
懒汉式(线程安全)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
在getInstance()方法上加了Synchronized关键字。保证了多线程情况下的线程安全。
写到这基本就介绍完了我们最常见的几种单例模式。
双重校验锁(线程安全,非常重要)
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这种情况下双重校验,首先判断instance是否为null,不用每次都获取锁,减少了获取锁的次数,效率比较高。
有一点格外要注意,volatile关键字的使用。为什么要用volatile关键字进行修饰,因为我们知道volatile关键字可以保证禁止指令重排。对于instance = new Singleton()这行代码它可以分解为三个步骤:
1 分配内存
2 初始化对象
3 将instance指向刚刚分配的地址
如果不使用volatile关键字进行修饰,可能会出现重排序,例如从1-2-3 排序为1-3-2。假如此时有2个线程A,B。线程A在执行instance = new Singleton()这行代码时,B线程进来,而此时A执行完了1和3,没有执行2,此时B线程判断instance不为null 直接返回一个未初始化的对象,就会出现问题。