剑指Offer对答如流系列 - 实现Singleton模式

面试题2:实现Singleton模式

题目:设计一个类,咱们只能生成该类的一个实例。面试

因为设计模式在面向对象程序设计中起着举足轻重的做用,在面试过程当中不少公司都喜欢问一些与设计模式相关的问题。在经常使用的模式中,Singleton是惟一一个可以用短短几十行代码完整实现的模式。所以,写一个Singleton的类型是一个很常见的面试题。编程

在这里插入图片描述

若是你看过我以前写的设计模式专栏,那么这道题思路你会很开阔。设计模式

单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行建立这个实例;三是它必须自行向整个系统提供这个实例安全

咱们下面来看一下它的实现多线程

1、懒汉式写法

public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    private LazySingleton() {

    }

    public static LazySingleton getInstance() {
        if(lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

关键就是将构造器私有,限制只能经过内部静态方法来获取一个实例。并发

可是这种写法,很明显不是线程安全的。若是多个线程在该类初始化以前,有大于一个线程调用了getinstance方法且lazySingleton == null 判断条件都是正确的时候,这个时候就会致使new出多个LazySingleton实例。能够这么改一下:ide

这种写法叫作DoubleCheck。针对类初始化以前多个线程进入 if(lazySingleton == null) 代码块中状况函数

这个时候加锁控制,再次判断 if(lazySingleton == null) ,若是条件成立则new出来一个实例,轮到其余的线程判断的时候天然就就为假了,问题大体解决。测试

public class LazyDoubleCheckSingleton {

    private static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton() {

    }

    public static LazyDoubleCheckSingleton getInstance() {
        if(lazySingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazySingleton == null) {
                    lazySingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazySingleton;
    }
}

可是即便是这样,上面代码的改进有些问题仍是没法解决的。

由于会有重排序问题。重排序是一种编译优化技术,属于《编译原理》的内容了,这里不详细探讨,可是要告诉你怎么回事。

正常来讲,下面的这段代码

lazySingleton = new LazyDoubleCheckSingleton();

执行的时候是这样的:
1.分配内存给这个对象
2.初始化对象
3.设置LazyDoubleCheckSingleton指向刚分配的内存地址。

可是编译优化后,多是这种样子
1.分配内存给这个对象
3.设置LazyDoubleCheckSingleton指向刚分配的内存地址。
2.初始化对象

2 步骤 和 3 步骤一反,就出问题了。(前提条件,编译器进行了编译优化)
好比说有两个线程,名字分别是线程1和线程2,线程1进入了 if(lazySingleton == null) 代码块,拿到了锁,进行了new LazyDoubleCheckSingleton()的执行,在加载构造类的实例的时候,设置LazyDoubleCheckSingleton指向刚分配的内存地址,可是尚未初始化对象。线程2判断 if(lazySingleton == null) 为假,直接返回了lazySingleton,又进行了使用,使用的时候就会出问题了。

画两张图吧:

重排序的状况以下:

在这里插入图片描述
再看出问题的地方

在这里插入图片描述
固然这个很好改进,从禁用重排序方面下手,添加一个volatile。不熟悉线程安全能够参考这篇文章【Java并发编程】线程安全性详解

private volatile static LazyDoubleCheckSingleton lazySingleton = null;

方法不止一种嘛,也能够利用对象初始化的“可见性”来解决,具体来讲是利用静态内部类基于类初始化的延迟加载,名字很长,可是理解起来并不困难。(使用这种方法,没必要担忧上面编译优化带来的问题)

类初始化的延迟加载与JVM息息相关,咱们演示的例子的只是被加载了而已,而没有连接和初始化。

咱们看一下实现方案:
定义一个静态内部类,其静态字段实例化了一个单例。获取单例须要调用getInstance方法间接获取。

public class StaticInnerClassSingleton {

    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }
}

若是对内部类不熟悉,能够参考这篇文章【Java核心技术卷】深刻理解Java的内部类

在这里插入图片描述
懒汉式的介绍就到这里吧,下面再看看另一种单例模式的实现


2、饿汉式写法

演示一下基本的写法

public class HungrySingleton {

    // 类加载的时候初始化
    private final static HungrySingleton hungrySingleton = new HungrySingleton();

    /*
    也能够在静态块里进行初始化
      private static HungrySingleton hungrySingleton;

     static {
        hungrySingleton = new HungrySingleton();
     }
     */
    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

}

饿汉式在类加载的时候就完成单例的实例化,若是用不到这个类会形成内存资源的浪费,由于单例实例引用不可变,因此是线程安全的

一样,上面的饿汉式写法也是存在问题的

咱们依次看一下:

首先是序列化破坏单例模式

先保证饿汉式可以序列化,须要继承Serializable 接口。

import java.io.Serializable;

public class HungrySingleton implements Serializable {

    // 类加载的时候初始化
    private final static HungrySingleton hungrySingleton = new HungrySingleton();

    /*
    也能够在静态块里进行初始化
      private static HungrySingleton hungrySingleton;

     static {
        hungrySingleton = new HungrySingleton();
     }
     */
    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

}

咱们测试一下:

import lombok.extern.slf4j.Slf4j;

import java.io.*;

@Slf4j
public class Test {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
       HungrySingleton hungrySingleton = HungrySingleton.getInstance();
       ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
       oos.writeObject(hungrySingleton);

       File file = new File("singleton");
       ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));

        HungrySingleton newHungrySingleton = (HungrySingleton) ois.readObject();

        log.info("结果 {}",hungrySingleton);
        log.info("结果 {}",newHungrySingleton);
        log.info("对比结果 {}",hungrySingleton == newHungrySingleton);
    }
}

结果:
在这里插入图片描述
结果发现对象不同,缘由就涉及到序列化的底层缘由了,咱们先看解决方式:

饿汉式代码中添加下面这段代码

private Object readResolve() {
        return hungrySingleton;
    }

从新运行,这个时候的结果:

在这里插入图片描述
缘由出在readResolve方法上,下面去ObjectInputStream源码部分找找缘由。(里面都涉及到底层实现,不要期望看懂)

在一个读取底层数据的方法上有一段描述
就是序列化的Object类中可能定义有一个readResolve方法。咱们在二进制数据读取的方法中看到了是否判断

在这里插入图片描述
private Object readOrdinaryObject()方法中有这段代码,若是存在ReadResolve方法,就去调用。不存在,不调用。联想到咱们在饿汉式添加的代码,大体能猜到怎么回事了吧。

在这里插入图片描述
***

另一种状况就是反射攻击破坏单例

演示一下

import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

@Slf4j
public class Test {

    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = HungrySingleton.class;

        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true); // 强行打开构造器权限
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();

        log.info("结果{}",instance);
        log.info("结果{}",newInstance);
        log.info("比较结果{}",newInstance == instance);
    }
}

在这里插入图片描述
这里强行破开了private的构造方法的权限,使得能new出来一个单例实例,这不是咱们想看到的。

解决方法是在构造方法中抛出异常

private HungrySingleton() {
        if( hungrySingleton != null) {
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }

这个时候再运行一下

在这里插入图片描述
其实对于懒汉式也是有反射破坏单例的问题的,也能够采用相似抛出异常的方法来解决。

饿汉式单例与懒汉式单例类比较

  • 饿汉式单例类在本身被加载时就将本身实例化。单从资源利用效率角度来说,这个比懒汉式单例类稍差些。从速度和反应时间角度来说,则比懒汉式单例类稍好些。
  • 懒汉式单例类在实例化时,必须处理好在多个线程同时首次引用此类时的访问限制问题,特别是当单例类做为资源控制器在实例化时必然涉及资源初始化,而资源初始化颇有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,须要经过同步化机制进行控制。

3、枚举

除此以外还有一种单例模式的实现就是枚举

使用枚举的方式实现单例模式是《Effective Java》做者力推的方式,在不少优秀的开源代码中常常能够看到使用枚举方式实现单例模式的地方,枚举类型不容许被继承,一样是线程安全的且只能被实例化一次,可是枚举类型不可以懒加载,对Singleton主动使用,好比调用其中的静态方法则INSTANCE会当即获得实例化。

//枚举类型自己是final的,不容许被继承
public enum Singleton
{
    INSTANCE;
    //实例变量
    private byte[] data = new byte[1024];

    Singleton()
    {
        System.out.println("I want to follow Jeffery.");
    }

    public static void method()
    {
        //调用该方法则会主动使用Singleton,INSTANCE将会被实例化
    }

    public static Singleton getInstance()
    {
        return INSTANCE;
    }
}

在实际面试中,咱们为了展示枚举单例模式,能够写成这样:

public enum Singleton
{
    INSTANCE;
  
    public static Singleton getInstance()
    {
        return INSTANCE;
    }
}

Java中的枚举实际上是一种语法糖,换句话说就是编译器帮助咱们作了一些的事情,咱们将字节码反编译成Java代码,看看编译器帮咱们作了什么,以及探讨为何使用枚举的方式实现单例模式是《Effective Java》做者力推的方式?

原始代码以下:

public enum EnumClass {
    SPRING,SUMMER,FALL,WINTER;
}

反编译后的代码

public final class EnumClass extends Enum
{

    public static EnumClass[] values()
    {
        return (EnumClass[])$VALUES.clone();
    }

    public static EnumClass valueOf(String name)
    {
        return (EnumClass)Enum.valueOf(suger/EnumClass, name);
    }

    private EnumClass(String s, int i)
    {
        super(s, i);
    }

    public static final EnumClass SPRING;
    public static final EnumClass SUMMER;
    public static final EnumClass FALL;
    public static final EnumClass WINTER;
    private static final EnumClass $VALUES[];

    static 
    {
        SPRING = new EnumClass("SPRING", 0);
        SUMMER = new EnumClass("SUMMER", 1);
        FALL = new EnumClass("FALL", 2);
        WINTER = new EnumClass("WINTER", 3);
        $VALUES = (new EnumClass[] {
            SPRING, SUMMER, FALL, WINTER
        });
    }
}

对于静态代码块不了解的参考 : Java中静态代码块、构造代码块、构造函数、普通代码块

结合前面的内容,是否是很容易理解了? 除此以外,咱们还能够看出,枚举是继承了Enum类的,同时它也是final,即不可继承的。

枚举类型的单例模式的玩法有不少,网上传的比较多的有如下几种:

内部枚举类形式

1.构造方法中实例化对象(上面提到了 注意了吗)

public class EnumSingleton {
    private EnumSingleton(){}
    
    public static EnumSingleton getInstance(){
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton{
        INSTANCE;
        
        private EnumSingleton singleton;
        
        //JVM会保证此方法绝对只调用一次
        Singleton(){
            singleton = new EnumSingleton();
        }
        public EnumSingleton getInstance(){
            return singleton;
        }
    }
}

2.枚举常量的值即为对象实例

public class EnumSingleton {
    private EnumSingleton(){}
    
    public static EnumSingleton getInstance(){
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton{
        INSTANCE(new EnumSingleton());
        private EnumSingleton singleton;
        
        //JVM会保证此方法绝对只调用一次
        Singleton(EnumSingleton singleton){
            this.singleton = singleton;
        }
        public EnumSingleton getInstance(){
            return singleton;
        }
    }
}

接口实现形式
对于一个标准的enum单例模式,最优秀的写法仍是实现接口的形式:

// 定义单例模式中须要完成的代码逻辑
public interface MySingleton {
    void doSomething();
}

public enum Singleton implements MySingleton {
    INSTANCE {
        @Override
        public void doSomething() {
            System.out.println("I want to follow Jeffery. What about you ?");
        }
    };

    public static MySingleton getInstance() {
        return Singleton.INSTANCE;
    }
}

我就问!单例模式的面试,你还怕不怕?

相关文章
相关标签/搜索