vlambda博客
学习文章列表

经典算法题: 实现Singleton单例模式

渣硕笔记

剑指 offer 经典题系列

面试题2: 实现单例模式。即设计一个类,任何时候我们都只能生成该类的一个实例。

题目解析



单例模式是常见常考的设计模式,它的实现的方法也有多种,本文将对各种方法进行介绍。


代码最优解


推荐解法一

双重校验加锁

 1public class Singleton {
2    private volatile static Singleton instance = null;
3
4    private Singleton() {}
5
6    public static Singleton getInstance() {
7        // 先检查实例是否存在,如果不存在才进入下面的同步块
8        if (instance == null) {
9            // 同步块,线程安全的创建实例
10            synchronized (Singleton.class) {
11                // 再次检查实例是否存在,如果不存在,才真正地创建实例
12                if (instance == null) {
13                    instance = new Singleton();
14                }
15            }
16        }
17        return instance;
18    }
19}


代码理解

之所以这段代码叫做双重校验加锁,就是因为使用了内外两层if条件判断,并在两次判断中间加锁。下面具体解析:


(1). 第一层校验(外层判断是否为null)的作用 ?

它的作用是避免每次进来都要加锁或者等待锁。当我们的实例化一个单例之后,其他后续的所有请求都没必要在进入同步代码块继续往下执行了,直接返回我们曾生成的实例即可,只有实例还未创建时才进入同步代码块。


(2). 第二层校验(内层判断是否为null)的作用 ?

它的作用是保证线程安全。假设我们去掉第二层校验,会出现以下错误情况:A线程和B线程都在同步块外面判断了instance为null,结果t1线程首先获得了线程锁,进入了同步块,然后t1线程会创造一个实例,此时instance已经被赋予了实例,t1线程退出同步块,直接返回了第一个创造的实例,此时t2线程获得线程锁,也进入同步块,此时t1线程其实已经创造好了实例,t2线程正常情况应该直接返回的,但是因为同步块里没有判断是否为null,直接就是一条创建实例的语句,所以t2线程也会创造一个实例返回,此时就造成创造了多个实例的情况。


(3). 为什么变量修饰为volatile ?

虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作,在多线程的场景下可能会由于指令重排序造成错误。因此,需要使用volatile保证每次读取变量值,都直接从内存中读取,避免重排序影响。


(4). 为什么变量和方法都需要定义为static ?

static关键字则用于定义静态变量和静态方法。


静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。


静态方法不依赖于任何对象就可以进行访问,方法被类所拥有。另外,值得注意的,非静态方法无法访问静态变量。


推荐解法二

静态内部类

 1public class SingletonDemo {
2    private static class SingletonHolder{
3        private static SingletonDemo instance=new SingletonDemo();
4    }
5    private SingletonDemo(){
6        private static SingleTon INSTANCE = new SingleTon();
7    }
8    public static SingletonDemo getInstance(){
9        return SingletonHolder.instance;
10    }
11}


代码理解

使用静态内部类的好处是:静态内部类不会在类加载时就加载,而是在调用getInstance()方法时才进行加载,达到了类似懒汉模式的效果,而这种方法又是线程安全的。


静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。



写在最后


单例模式题目除了写代码之后,可能在面试中还会考察对单例模式的理解,后续将进行相关内容介绍。