关于java:字节面试官写一个你认为最好的单例模式于是我写了7个

33次阅读

共计 12276 个字符,预计需要花费 31 分钟才能阅读完成。

面试题:写一个你认为最好的单例模式

面试考察点

考查目标:单例模式能够考查十分多的基础知识,因而对于这种问题,很多面试官都会问。小伙伴要留神,在面试过程中,凡是可能从多个维度考查求职者能力的题目,肯定不会被摈弃,特地是比拟泛的问题,比方:”请你说说对 xxx 的了解“之类。

考查范畴:工作 1 到 5 年教训,随着教训的晋升,对于该问题的考查深度越深。

背景常识

单例模式,是一种软件设计模式,属于创立型模式的一种。

它的个性是:保障一个类只有惟一的一个实例,并提供一个全局的拜访点。

基于这个个性能够晓得,单例模式的益处是,能够防止对象的频繁创立对于内存的耗费,因为它限度了实例的创立,总的来说,它有以下益处:

  1. 管制资源的应用,通过线程同步来管制资源的并发拜访;
  2. 管制实例产生的数量,达到节约资源的目标。
  3. 作为通信媒介应用,也就是数据共享,它能够在不建设间接关联的条件下,让多个不相干的两个线程或者过程之间实现通信。

在理论利用中,单例模式应用最多的就是在 Spring 的 IOC 容器中,对于 Bean 的治理,默认都是单例。一个 bean 只会创立一个对象,存在内置 map 中,之后无论获取多少次该 bean,都返回同一个对象。

上面来理解单例模式的设计。

单例模式设计

既然要保障一个类在运行期间只有一个实例,那必然不能应用 new 关键字来进行实例。

所以,第一步肯定是私有化该类的构造方法,这样就避免了调用方本人创立该类的实例。

接着,因为内部无奈实例化该对象,因而必须从外部实例化之后,提供一个全局的拜访入口,来获取该类的全局惟一实例,因而咱们能够在类的外部定义一个动态变量来援用惟一的实例,作为对外提供的实例拜访对象。基于这些点,咱们能够失去如下设计。

public class Singleton {
    // 动态字段援用惟一实例:
    private static final Singleton INSTANCE = new Singleton();

    // private 构造方法保障内部无奈实例化:
    private Singleton() {}
}

接着,还须要给内部一个拜访该对象实例 INSTANCE 的办法,咱们能够提供一个静态方法

public class Singleton {
    // 动态字段援用惟一实例:
    private static final Singleton INSTANCE = new Singleton();

    // 通过静态方法返回实例:
    public static Singleton getInstance() {return INSTANCE;}

    // private 构造方法保障内部无奈实例化:
    private Singleton() {}
}

这样就实现了单例模式的设计,总结来看,单例模式分三步骤。

  1. 应用 private 私有化构造方法,确保内部无奈实例化;
  2. 通过 private static 变量持有惟一实例,保障全局唯一性;
  3. 通过 public static 办法返回此惟一实例,使内部调用方能获取到实例。

单例模式的其余实现

既然单例模式只须要保障程序运行期间只会产生惟一的实例,那意味着单例模式还有更多的实现办法。

  • 懒汉式单例模式
  • 饿汉式单例模式
  • DCL 双重查看式单例
  • 动态外部类
  • 枚举单例
  • 基于容器实现单例

懒汉式单例模式

懒汉式,示意不提前创建对象实例,而是在须要的时候再创立,代码如下。

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // synchronized 办法, 多线程状况下保障单例对象惟一
    public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();
        }
        return instance;
    }
}

其中,对 getInstance() 办法,减少了 synchronized 同步关键字,目标是为了防止在多线程环境下同一时刻调用该办法导致呈现多实例问题(线程的并行执行个性带来的线程安全性问题)。

长处: 只有在应用时才会实例化单例,肯定水平上节约了内存资源。毛病: 第一次加载时要立刻实例化,反馈稍慢 。每次调用 getInstance() 办法都会进行同步,这样会 耗费不必要的资源 这种模式个别不倡议应用。

DCL 双重查看式单例

DCL 双重查看式单例模式,是基于饿汉式单例模式的性能优化版本。

/**
 * DCL 实现单例模式
 */
public class Singleton {
    private static volatile Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        // 两层判空,第一层是为了防止不必要的同步
        if (instance == null) {synchronized (Singleton.class) {if (instance == null) {// 第二层是为了在 null 的状况下创立实例
                    instance = new Singleton();}
            }

        }
        return instance;
    }
}

从代码中能够看到,DCL 模式做了两处改良:

  1. getInstance() 办法中,把 synchronized 同步锁的加锁范畴放大了。

    放大锁的范畴可能带来性能上的晋升,无妨思考一下,在原来的 懒汉式 模式中,把 synchronized 关键字加载办法级别上,意味着不论是多线程环境还是单线程环境,任何一个调用者须要取得这个对象实例时,都须要取得锁。然而加这个锁其实只有在第一次初始化该实例的时候起到爱护作用。后续的拜访,应该间接返回 instance 实例对象就行。所以把 synchroinzed 加在办法级别,在多线程环境中必然会带来性能上的开销。

    而 DCL 模式的革新,就是放大了加锁的范畴,只须要爱护该实例对象 instance 在第一次初始化即可,后续的拜访,都不须要去竞争同步锁。因而它的设计是:

    • 先判断 instance 实例是否为空,如果是,则减少 synchronized 类级别锁,爱护 instance 对象的实例化过程,防止在多线程环境下呈现多实例问题。
    • 接着再 synchronized 同步关键字范畴内,再一次判断 instance 实例是否为空,同样也是为了防止临界点时,上一个线程刚初始化实现,下一个线程进入到同步代码块导致多实例问题。
  2. 在成员变量 instance 上润饰了 volatile 关键字,该关键字是为了保障可见性。

    之所以要加这个关键字,是为了防止在 JVM 中指令重排序带来的可见性问题,这个问题次要体现在 instance=new Singleton() 这段代码中。咱们来看这段代码的字节码

     17: new           #3                  // class org/example/cl04/Singleton
     20: dup
     21: invokespecial #4                  // Method "<init>":()V
     24: putstatic     #2                  // Field instance:Lorg/example/cl04/Singleton;
     27: aload_0
     28: monitorexit
     29: goto          37
     32: astore_1
     33: aload_0

    关注以下几个指令

    invokespecial #4 指令,和 astore_1 指令,是容许重排序的(对于重排序问题,就不再本篇文章中阐明,后续的面试题中会剖析到),就是说执行程序有可能 astore_1 先执行,invokespecial #1后执行。

    重排序对于两个没有依赖关系的指令操作,CPU 和内存以及 JVM,为了优化程序执行性能,会对执行指令进行重排序。也就是说两个指令的执行程序不肯定会依照程序编写程序来执行。

    因为在堆上建设对象开拓地址当前,地址就曾经定了,而 “将栈里的 Singleton instance 与堆上的对象建设起援用关联”和“将对象里的成员变量进行赋值操作”是没什么逻辑关系的。

    所以 cpu 能够进行乱序执行,只有程序最终的后果是统一的就能够。

    这种状况,在单线程下没有问题,然而多线程下,就会呈现谬误。

    试想一下,DCL 下,线程 A 在将对象 new 进去的时,刚执行完 new #4 指令,紧接着没有执行 invokespecial #4 指令,而是执行了astore_1,也就是说产生了指令重排序。

    此时线程 B 进入 getInstance(),发现 instance 并不为空(因为曾经有了援用指向了对象,只不过还没来得及给对象里的成员变量赋值),而后线程 B 便间接 return 了一个“半初始化”对象(对象还没彻底创立完)。

    所以 DCL 里,须要给 instance 加上 volatile 关键字,因为 volatile 在 JVM 层有一个个性叫内存屏障,能够避免指令重排序,从而保障了程序的正确性。

    • new #3:这行指令是说在堆上的某个地址处开拓了一块空间作为 Singleton 对象
    • invokespecial #4:这行指令是说将对象里的成员变量进行赋值操作
    • astore_1:这行指令是说将栈里的 Singleton instance 与堆上的对象建设起援用关联

对于 DCL 模式的优缺点:

长处:资源利用率高,既可能在须要的时候才初始化实例,又能保障线程平安,同时调用 getInstance()办法 不进行同步锁 ,效率高。 毛病:第一次加载时稍慢,因为Java 内存模型的起因偶然会失败。在高并发环境下也有肯定的缺点,尽管产生概率很小。

DCL 模式是应用最多的单例模式实现形式,除非代码在 并发场景比较复杂,否则,这种形式根本都能满足需要。

饿汉式单例模式

在类加载的时候不创立单例实例。只有在第一次申请实例的时候的时候创立,并且只在第一次创立后,当前不再创立该类的实例。

/**
 * 饿汉式实现单例模式
 */
public class Singleton {private static Singleton instance = new Singleton();

    private Singleton() {}

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

因为 static 关键字润饰的属性,示意这个成员属于类自身,不属于实例,运行时,Java 虚拟机只为动态变量调配一次内存,在类加载的过程中实现动态变量的内存调配。

所以在类加载的时候就创立好对象实例,后续在拜访时间接获取该实例即可。

而该模式的优缺点也非常明显。

长处:线程平安,不须要思考并发安全性。

毛病:节约内存空间,不论该对象是否被应用到,都会在启动时提前分配内存空间。

动态外部类

动态外部类,是基于饿汉式模式下的优化。

第一次加载 Singleton 类时不会初始化 instance,只有在第一次调用 getInstance()办法时,虚构机会加载 SingletonHolder 类,初始化instanceinstance 的唯一性、创立过程的线程安全性,都由 JVM 来保障。

/**
 * 动态外部类实现单例模式
 */
public class Singleton {private Singleton() { }

  public static Singleton getInstance() {return SingletonHolder.instance;}

  /**
     * 动态外部类
     */
  private static class SingletonHolder {private static Singleton instance = new Singleton();
  }
}

这种形式既保证线程平安,单例对象的惟一,也提早了单例的初始化,举荐应用这种形式来实现单例模式。

动态外部类不会因为外部类的加载而加载,同时动态外部类的加载不须要附丽外部类,在应用时才加载,不过在加载动态外部类的过程中也会加载外部类

知识点:如果用 static 来润饰一个外部类,那么就是动态外部类。这个外部类属于外部类自身,然而不属于外部类的任何对象。因而应用 static 润饰的外部类称为动态外部类。动态外部类有如下规定:

  • 动态外部类不能拜访外部类的实例成员,只能拜访外部类的类成员。
  • 外部类能够应用动态外部类的类名作为调用者来拜访动态外部类的类成员,也能够应用动态外部类对象拜访其实例成员。

动态外部类单例长处

  • 对象的创立是线程平安的。
  • 反对延时加载。
  • 获取对象时不须要加锁。

这是一种比拟罕用的模式之一。

基于枚举实现单例

用枚举来实现单例,是最简略的形式。这种实现形式通过 Java 枚举类型自身的个性,保障了实例创立的线程安全性和实例的唯一性。

public enum SingletonEnum {

    INSTANCE;

    public void execute(){System.out.println("begin execute");
    }

    public static void main(String[] args) {SingletonEnum.INSTANCE.execute();
    }
}

基于枚举实现单例会发现它并不需要后面形容的几个操作

  1. 构造方法私有化
  2. 实例化的变量援用私有化
  3. 获取实例的办法共有

这类的形式实现枚举其实并不保险,因为 私有化结构 并不能抵挡 反射攻打.

这种形式是 Effective Java 作者 Josh Bloch 提倡的形式,它不仅能防止多线程同步问题,而且还能避免反序列化 从新创立新的对象,堪称是很刚强的壁垒啊。

基于容器实现单例

上面的代码演示了基于容器的形式来治理单例。

import java.util.HashMap;
import java.util.Map;
/**
 * 容器类实现单例模式
 */
public class SingletonManager {private static Map<String, Object> objMap = new HashMap<String, Object>();

    public static void regsiterService(String key, Object instance) {if (!objMap.containsKey(key)) {objMap.put(key, instance);
        }
    }

    public static Object getService(String key) {return objMap.get(key);
    }
}

SingletonManager 能够 治理多个单例类型,在程序的初始化时,将多个单例类型注入到一个对立治理的类中,应用时依据 key 获取对象对应类型的对象。这种形式能够通过对立的接口获取操作,暗藏了具体实现,升高了耦合度。

对于单例模式的毁坏

后面在剖析枚举类实现单例模式时,有提到一个问题,就是私有化结构,会被反射毁坏,导致呈现多实例问题。

public class Singleton {

    private static volatile Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        // 两层判空,第一层是为了防止不必要的同步
        if (instance == null) {synchronized (Singleton.class) {if (instance == null) {// 第二层是为了在 null 的状况下创立实例
                    instance = new Singleton();}
            }
        }
        return instance;
    }

    public static void main(String[] args) throws Exception{Singleton instance=Singleton.getInstance();
        Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton refInstance=constructor.newInstance();
        System.out.println(instance);
        System.out.println(refInstance);
        System.out.println(instance==refInstance);

    }
}

运行后果如下

org.example.cl04.Singleton@29453f44
org.example.cl04.Singleton@5cad8086
false

因为反射能够毁坏 private 个性,所以但凡通过 private 私有化结构实现的单例模式,都可能被反射毁坏从而呈现多实例问题。

可能有人会问,咱们没事干嘛要去毁坏单例呢?间接基于这个入口拜访就不会有问题啊?

实践上来说是这样,然而,假如遇到上面这种状况呢?

上面的代码演示的是通过对象流实现 Singleton 的序列化和反序列化。

public class Singleton implements Serializable {

    private static volatile Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        // 两层判空,第一层是为了防止不必要的同步
        if (instance == null) {synchronized (Singleton.class) {if (instance == null) {// 第二层是为了在 null 的状况下创立实例
                    instance = new Singleton();}
            }
        }
        return instance;
    }

    public static void main(String[] args) throws Exception {Singleton instance=Singleton.getInstance();
        ByteArrayOutputStream baos=new ByteArrayOutputStream();
        ObjectOutputStream oos=new ObjectOutputStream(baos);
        oos.writeObject(instance);
        ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois=new ObjectInputStream(bais);
        Singleton ri=(Singleton) ois.readObject();
        System.out.println(instance);
        System.out.println(ri);
        System.out.println(instance==ri);
    }
}

运行后果如下

org.example.cl04.Singleton@36baf30c
org.example.cl04.Singleton@66a29884
false

能够看到,序列化的形式,也会毁坏单例模式。

枚举类单例的毁坏测试

可能有人会问,枚举难道就不能毁坏吗?

咱们能够试试看,代码如下。

public enum SingletonEnum {

    INSTANCE;

    public void execute(){System.out.println("begin execute");
    }

    public static void main(String[] args) throws Exception{
        SingletonEnum instance=SingletonEnum.INSTANCE;
        Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        SingletonEnum refInstance=constructor.newInstance();
        System.out.println(instance);
        System.out.println(refInstance);
        System.out.println(instance==refInstance);
    }
}

运行后果如下

Exception in thread "main" java.lang.NoSuchMethodException: org.example.cl04.SingletonEnum.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at org.example.cl04.SingletonEnum.main(SingletonEnum.java:15)

从谬误来看,仿佛是没有一个空的构造函数?这里并没有证实 反射无奈毁坏单例。

上面是 Enum 这类的源码,所有枚举类都继承了 Enum 这个抽象类。

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
    /**
     * The name of this enum constant, as declared in the enum declaration.
     * Most programmers should use the {@link #toString} method rather than
     * accessing this field.
     */
    private final String name;

    /**
     * Returns the name of this enum constant, exactly as declared in its
     * enum declaration.
     *
     * <b>Most programmers should use the {@link #toString} method in
     * preference to this one, as the toString method may return
     * a more user-friendly name.</b>  This method is designed primarily for
     * use in specialized situations where correctness depends on getting the
     * exact name, which will not vary from release to release.
     *
     * @return the name of this enum constant
     */
    public final String name() {return name;}

    /**
     * The ordinal of this enumeration constant (its position
     * in the enum declaration, where the initial constant is assigned
     * an ordinal of zero).
     *
     * Most programmers will have no use for this field.  It is designed
     * for use by sophisticated enum-based data structures, such as
     * {@link java.util.EnumSet} and {@link java.util.EnumMap}.
     */
    private final int ordinal;

    /**
     * Returns the ordinal of this enumeration constant (its position
     * in its enum declaration, where the initial constant is assigned
     * an ordinal of zero).
     *
     * Most programmers will have no use for this method.  It is
     * designed for use by sophisticated enum-based data structures, such
     * as {@link java.util.EnumSet} and {@link java.util.EnumMap}.
     *
     * @return the ordinal of this enumeration constant
     */
    public final int ordinal() {return ordinal;}

    /**
     * Sole constructor.  Programmers cannot invoke this constructor.
     * It is for use by code emitted by the compiler in response to
     * enum type declarations.
     *
     * @param name - The name of this enum constant, which is the identifier
     *               used to declare it.
     * @param ordinal - The ordinal of this enumeration constant (its position
     *         in the enum declaration, where the initial constant is assigned
     *         an ordinal of zero).
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
}

该类有一个惟一的构造方法,承受两个参数别离是:nameordinal

那咱们尝试通过这个构造方法来创立一下实例,演示代码如下。

public enum SingletonEnum {

    INSTANCE;

    public void execute(){System.out.println("begin execute");
    }

    public static void main(String[] args) throws Exception{
        SingletonEnum instance=SingletonEnum.INSTANCE;
        Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        SingletonEnum refInstance=constructor.newInstance("refinstance",2);
        System.out.println(instance);
        System.out.println(refInstance);
        System.out.println(instance==refInstance);
    }
}

运行上述代码,执行后果如下

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at org.example.cl04.SingletonEnum.main(SingletonEnum.java:17)

从错误信息来看,咱们胜利获取到了 Constructor 这个结构器,然而在 newInstance 时报错。

定位到出错的源码地位。

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile

从这段代码:(clazz.getModifiers() & Modifier.ENUM) != 0阐明:反射在通过 newInstance 创建对象时,会查看该类是否 ENUM 润饰,如果是则抛出异样,反射失败,因而枚举类型对反射是相对平安的。

既然反射无奈毁坏?那序列化呢?咱们再来试试

public enum SingletonEnum {

    INSTANCE;

    public void execute(){System.out.println("begin execute");
    }
    public static void main(String[] args) throws Exception{
        SingletonEnum instance=SingletonEnum.INSTANCE;
        ByteArrayOutputStream baos=new ByteArrayOutputStream();
        ObjectOutputStream oos=new ObjectOutputStream(baos);
        oos.writeObject(instance);
        ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois=new ObjectInputStream(bais);
        SingletonEnum ri=(SingletonEnum) ois.readObject();
        System.out.println(instance);
        System.out.println(ri);
        System.out.println(instance==ri);
    }
}

运行后果如下.

INSTANCE
INSTANCE
true

因而,咱们能够得出一个论断,枚举类型是所有单例模式中惟一可能防止反射毁坏导致多实例问题的设计模式。

综上,能够得出结论:枚举是实现单例模式的最佳实际。毕竟应用它全都是长处:

  1. 反射平安
  2. 序列化 / 反序列化平安
  3. 写法简略

问题解答

面试题:写一个你认为最好的单例模式

对于这个问题,想必大家都有答案了,枚举形式实现单例才是最好的。

当然,答复的时候要从全方面角度去解说。

  1. 单例模式的概念
  2. 有哪些形式实现单例
  3. 每种单例模式的优缺点
  4. 最好的单例模式,以及为什么你感觉它是最好的?

问题总结

单例模式看起来简略,然而学到极致,也还是有很多知识点的。

比方波及到线程平安问题、静态方法和动态成员变量的特色、枚举、反射等。

多想再回到从前,大家都只用 jsp/servlet,没有这么多乌七八糟的常识,咱们只想做个简略的程序员。

正文完
 0