题目:设计一个类,我们只能生成该类的一个实例。
单例模式的实现要点:
① 构造函数设为私有函数,保证只能生成一个实例
② 是否有线程安全问题
③ 建议将实例定义为静态的,在需要的时候创建该实例
解法一:
class Singleton1{
private Singleton1(){
}
private static Singleton1 instance = null;
public static Singleton1 getInstance() {
if(instance == null)
instance = new Singleton1();
return instance;
}
}
以上代码是单例模式的懒汉式实现。
懒汉式和饿汉式单例模式对比:
① 懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。如果一直未被访问则节约了内存空间。
② 饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。
③ 从线程安全角度来说,不加同步的懒汉式是线程不安全的;饿汉式是线程安全的,因为虚拟机保证只会装载一次,在装载类的时候是不会发生并发的。
解法一存在的问题:解法一中的代码在单线程时工作正常,但在多线程的情况下就会有问题。如果两个线程同时运行到判断 instance 是否为 null 的 if 语句,并且 instance 的确没有被创建时,那么两个线程都会创建一个实例,此时解法一就不再满足单例模式的要求了。
解法二
class Singleton2{
private Singleton2(){
}
private static byte[] lockObject = new byte[1];//锁对象
private static Singleton2 instance = null;
public static Singleton2 getInstance() {
synchronized(lockObject) {
if(instance == null)
instance = new Singleton2();
}
return instance;
}
}
解法二改进了解法一,保证了在多线程环境下只能得到一个实例。如果此时有两个线程同时访问 getInstance()方法,由于在同一时刻只有一条线程能够得到同步锁。当一条线程访问时,另一条线程只能等待。因而不会出现创建多个实例的情况。
解法二存在的问题:每次通过getInstance()方法获取Singleton2实例时,都会试图加上一个同步锁,而加锁是一个非常耗时的操作。
解法三 - 双重检查锁定(DoubleCheckingLocking , DCL)
class Singleton3{
private Singleton3() {
}
private static byte[] lockObject = new byte[1];//锁对象
private static Singleton3 instance = null;
public static Singleton3 getInstance() {
if(instance == null)
synchronized(lockObject) {
if(instance == null)
instance = new Singleton3();
}
return instance;
}
}
解法三只有当 instance 为 null 时,才需要加锁操作。解法三双重检查锁定用加锁机制确保了在多线程环境下,只创建一个实例,并且用两个 if 判断来提高效率。
解法三存在的问题(面试难点):可能获取到 instance 不为 null 但是不完整的对象
instance = new Singleton3();并非原子操作,该对象实例化的过程可以分解为以下三个步骤
① 给 Singleton3 的实例分配内存空间
② 初始化对象
③ 将 instance 指向分配的内存空间(此时 instance 非空)
其中 ② 和 ③ 是可以重排序的,也就是说某个线程在第一次判断 if(instance == null) 时,引用可能不为 null 但是访问到的对象是未经过初始化的不完整对象。
解决办法:用 volatile 关键字修饰 instance,遵循 hapen-before 原则中的 volatile 变量规则(对一个volatile变量的写操作先行发生于后面对这个变量的读操作),即可保证不会读取到一个不完整的 instance 对象。正确的 DCL 单例模式代码如下:
class Singleton3{
private Singleton3() {
}
private static byte[] lockObject = new byte[1]; //锁对象
private volatile static Singleton3 instance = null; // 加了 volatile
public static Singleton3 getInstance() {
if(instance == null)
synchronized(lockObject) {
if(instance == null)
instance = new Singleton3();
}
return instance;
}
}
解法四
class Singleton4{
private Singleton4(){
}
private static Singleton4 instance = new Singleton4();
public static Singleton4 getInstance() {
return instance;
}
}
饿汉式加载是线程安全的。尽量不要使用延迟初始化,延迟初始化的适用场景:在初始化一些高开销对象的时候,只有在使用到时才对其进行初始化。
解法四存在的问题:过早地创建实例,从而降低了内存的使用效率。
解法五
class Singleton5 {
private Singleton5() {}
private static class SingletonInstance {
private static final Singleton5 INSTANCE = new Singleton5();
}
public static Singleton5 getInstance() {
return SingletonInstance.INSTANCE;
}
}
解法五跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要 Singleton 类被装载就会实例化,没有 Lazy-Loading 的作用,而静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,延迟加载,效率高。
解法比较
这五种实现单例模式的解法
第一种解法在多线程环境中不能正常工作;
第二种解法虽然能在多线程环境中正常工作,但时间效率很低。
----- 解法一和解法二都不会是面试官青睐的解法
第三种解法通过两次判断一次加锁确保在多线程环境中能高效率地工作。
----- 可能会被问到 DCL 的深层次问题,考察 JVM
第四种解法是单例模式的饿汉式实现,是线程安全的。
第五种解法利用了Java静态内部类的懒加载特性实现了单例模式的延迟加载,同时避免了线程不安全。
总结 : 手写单例模式时推荐写解法三 - DCL的正确版本,以此展开问题,获得面试官青睐!
最后祝大家在千军万马的面试大军中能脱颖而出,获得自己心仪的 offer!
整理不易,如果文章对您有帮助,可以赞赏我哟~~ 谢谢鼓励