面试题2:实现Singleton模式

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

由于设计模式在面向对象程序设计中起着举足轻重的作用,在面试过程中很多公司都喜欢问一些与设计模式相关的问题。在常用的模式中,Singleton是唯一一个能够用短短几十行代码完整实现的模式。因此,写一个Singleton的类型是一个很常见的面试题。

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

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

我们下面来看一下它的实现

懒汉式写法

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实例。可以这么改一下:

这种写法叫做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. 分配内存给这个对象
  2. 设置LazyDoubleCheckSingleton指向刚分配的内存地址。
  3. 初始化对象

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的内部类



懒汉式的介绍就到这里吧,下面再看看另外一种单例模式的实现


饿汉式写法

演示一下基本的写法

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("单例构造器禁止反射调用");
}
}

这个时候再运行一下

其实对于懒汉式也是有反射破坏单例的问题的,也可以采用类似抛出异常的方法来解决。

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

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

枚举

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

使用枚举的方式实现单例模式是《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;
}
}

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

剑指Offer对答如流系列 - 实现Singleton模式的更多相关文章

  1. 剑指 offer set 28 实现 Singleton 模式

    singleton 模式又称单例模式, 它能够保证只有一个实例. 在多线程环境中, 需要小心设计, 防止两个线程同时创建两个实例. 解法 1. 能在多线程中工作但效率不高 public sealed ...

  2. 剑指Offer对答如流系列 - 重建二叉树

    面试题6:重建二叉树 题目描述: 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字.例如输入前序遍历序列{1,2,4,7,3,5,6,8} ...

  3. 剑指offer题目系列三(链表相关题目)

    本篇延续上一篇剑指offer题目系列二,介绍<剑指offer>第二版中的四个题目:O(1)时间内删除链表结点.链表中倒数第k个结点.反转链表.合并两个排序的链表.同样,这些题目并非严格按照 ...

  4. 剑指offer题目系列二

    本篇延续上一篇,介绍<剑指offer>第二版中的四个题目:从尾到头打印链表.用两个栈实现队列.旋转数组的最小数字.二进制中1的个数. 5.从尾到头打印链表 题目:输入一个链表的头结点,从尾 ...

  5. 剑指offer题目系列一

    本篇介绍<剑指offer>第二版中的四个题目:找出数组中重复的数字.二维数组中的查找.替换字符串中的空格.计算斐波那契数列第n项. 这些题目并非严格按照书中的顺序展示的,而是按自己学习的顺 ...

  6. 剑指offer自学系列(三)

    题目描述: 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变,例如{5,1,4,2 ...

  7. 剑指offer-面试题2.实例Singleton模式

    题目:设计一个类,我们只能生成该类的一个实例 这道题显然是对设计模式的考察,很明显是单例模式.什么是单例模式呢,就是就像题目所说的只能生成一 个类的实例.那么我们不难考虑到下面几点: 1.不能new多 ...

  8. 剑指offer自学系列(五)

    题目描述:请实现一个函数用来找出字符流中第一个只出现一次的字符.例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g".当从该字符流中读出 ...

  9. 剑指offer自学系列(四)

    题目描述: 输入一个正整数数组,把数组里面所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个,例如输入数组{3,32,321},输出的最小数字为321323 题目分析: 如果采用穷举法,把 ...

随机推荐

  1. 文本框(代替input)输入长度限制、提示

    <div class="inform_content_text"> <textarea name="name" placeholder=&qu ...

  2. Team Foundation Server 2015使用教程【2】:默认团队成员添加

    官方文档:https://www.visualstudio.com/en-us/docs/setup-admin/add-users

  3. AbstactFactory模式

    AbstractFactory模式就是用来解决这类问题的:要创建一组相关或者相互依赖的对象. AbstractFactory模式关键就是将这一组对象的创建封装到一个用于创建对象的类(ConcreteF ...

  4. 010.MFC_Progress

    一.建立名为processCtrl的MFC工程,添加Progress Control 和 button控件.修改button Caption属性为start,ID属性为IDC_BTN_START 为进 ...

  5. 曹工说Spring Boot源码(6)-- Spring怎么从xml文件里解析bean的

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  6. 0005 表格table

    第01阶段.前端基础.表格 表格 table 目标: 理解: 能说出表格用来做什么的 表格的基本结构组成 应用: 能够熟练写出n行n列的表格 能简单的合并单元格 ​ 表格作用: 存在即是合理的. 表格 ...

  7. StatePattern(状态模式)-----Java/.Net

    在状态模式(State Pattern)中,类的行为是基于它的状态改变的.这种类型的设计模式属于行为型模式. 在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context ...

  8. MicroPython TPYBoard v702实现HTTP应用功能

    [Micropython]TPYBoard v702 HTTP应用功能 转载请注明文章来源,更多教程可自助参考docs.tpyboard.com,QQ技术交流群:157816561,公众号:Micro ...

  9. Vuex入门实践(中)-多module中的state、mutations、actions和getters

    一.前言 上一篇文章<Vuex入门实践(上)>,我们一共实践了vuex的这些内容: 1.在state中定义共享属性,在组件中可使用[$store.state.属性名]访问共享属性 2.在m ...

  10. Java之IO流用法总结

    Java的IO流概述:1.I/O是Input/Output的缩写,I/O技术是非常实用的技术,用于处理设备之间的数据传输.如读/写文件,网络通讯等.2.Java程序中,对于数据的输入/输出操作以“流( ...