百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

面试题02-玩转单例模式

toyiye 2024-06-21 11:59 7 浏览 0 评论

单例模式

1、什么叫单例模式

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意点:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。
  • 不需要实例化该类对象就可以访问

2、单例实现介绍

**实现意图:**保证一个类仅有一个实例,并提供一个访问它的全局访问点。

**主要解决:**一个全局使用的类频繁地创建与销毁。

**何时使用:**当您想控制实例数目,节省系统资源的时候。

**如何解决:**判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

**关键代码:**构造函数是私有的。

3、具体实现

3.1、饿汉式单例

3.1.1、代码实现

/**
 * @author luosong
 * @version 1.0
 * @date 2021/3/23 9:17
 * 饿汉式单例
 */
public class HungrySingle {
    // 构造方法私有化
    private HungrySingle(){

    }
	// 单例自己创建自已的对象
    private static HungrySingle hungrySingle = new HungrySingle();
	// 对外提供唯一的访问方式
    public static HungrySingle getInstance(){
        return hungrySingle;
    }
}

3.1.2、存在的问题

类加载的时候就去初始化类,如果对象并没有使用,浪费内存,增加服务器负担。

3.2、懒汉式单例

3.2.1、代码实现

/**
 * @author luosong
 * @version 1.0
 * @date 2021/3/23 9:16
 * 懒汉式单例
 */
public class LazySingle {
    private LazySingle(){

    }

    public static LazySingle lazySingle = null;

    public static LazySingle getInstance(){
        if (lazySingle == null){
            lazySingle = new LazySingle();
        }
        return lazySingle;
    }
}

3.2.2、存在的问题

懒汉式单例在类加载并不会创建,在使用调用getInstance()方法才会创建,但是存在线程安全的问题,多线程情况下可能会出现创建多个单例的情况

测试代码如下:

public class SingleTest {
    public static void main(String[] args) {
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(LazySingle.getLazySingle().hashCode());
                //System.out.println(HungrySingle.getHungrySingle().hashCode());
            }).start();
        }
    }
}

测试结果如下:在并发情况下懒汉模式并不安全,出现了不同hashCode值的对象,说明并没有保持单例模式的特性。

3.3、懒汉式单例synchronized

基于懒汉式存在的并发问题,加锁解决

3.3.1、代码实现

public class LazySingle {
    private LazySingle(){

    }

    public static LazySingle lazySingle = null;

    public static synchronized LazySingle getLazySingle(){
        if (lazySingle == null){
            lazySingle = new LazySingle();
        }
        return lazySingle;
    }
}

3.3.2、存在的问题

该单例模式确实可以解决并发的问题,但是synchronized是重量级锁,效率极低不推荐使用。

3.4、懒汉式单例DCL

基于上诉synchronized加锁造成的效率低下问题,提出双重锁的机制,同样有加锁的效果,但是效率高。

3.4.1、代码实现

public class LazySingle {
    private LazySingle(){

    }

    public static LazySingle lazySingle = null;

    public static synchronized LazySingle getLazySingle(){
        if (lazySingle == null){
            // 只要lazySingle为空就锁住该类,只允许当前线程操作
            synchronized (LazySingle.class){
                if (lazySingle == null){
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }
}

3.4.2、存在的问题

DCL双重锁,效率比synchronized有较大的提高,但是内部实现还是基于synchronized锁去实现,所以也不太可取。

3.5、懒汉式单例静态内部类

3.5.1、代码实现

这种方式利用了classloader机制来保证初始化LAZYSINGLE 时只有一个线程,LazySingleStaticClass类被装载了,LAZYSINGLE不一定被初始化。因为SingletonHolder类没有被主动使用,只有通过显式调用 getInstance方法时,才会显式装载LazySingleStaticClass类,从而实例化 LAZYSINGLE

public class LazySingleStaticClass {
    private LazySingleStaticClass(){

    }

    private static class SingletonHolder{
        private static LazySingleStaticClass LAZYSINGLE = new LazySingleStaticClass();
    }

    public static LazySingleStaticClass getInstance(){
        return SingletonHolder.LAZYSINGLE;
    }
}

4、破坏单例

4.1、指令重排volatile

创建对象的三个步骤

  1. 分配内存空间
  2. 执行构造方法,初始化对象
  3. 把这个对象指向这个空间

在多线程情况下可能会出现指令重排情况本来顺序执行是123,可能因为指令重排执行顺序会变成132针对上诉问题,lazySingle应该加上关键字volatile

public static volatile LazySingle lazySingle = null;

4.2、反射

4.2.1、代码实现

根据反射来破坏单例,引用DCL双重锁单例测试

public class LazySingle {
    private LazySingle(){

    }

    public static volatile LazySingle lazySingle = null;

    public static synchronized LazySingle getLazySingle(){
        if (lazySingle == null){
            synchronized (LazySingle.class){
                if (lazySingle == null){
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }
}

反射破坏单例代码

public class ReflectionTest {
    // 两个对象返回的结果果然不一样,因为反射绕开了getLazySingle方法走了私有的构造方法
    public static void main(String[] args) throws Exception {
        LazySingle lazySingle = LazySingle.getLazySingle();

        Class<? extends LazySingle> aClass = lazySingle.getClass();
        Constructor<? extends LazySingle> declaredConstructor = aClass.getDeclaredConstructor(null);
        // 私有方法开启安全模式
        declaredConstructor.setAccessible(true);
        LazySingle lazySingle1 = declaredConstructor.newInstance(null);
        System.out.println(lazySingle==lazySingle1);// false
    }
}

4.2.2、解决办法

public class LazySingle {
    // flag 必须是static的,不然每个对象都会有一份,构造里面的判断逻辑没什么用
    private static Boolean flag = true;

    private LazySingle(){
        synchronized (LazySingle.class){
            if (flag){
                // 第一次通过getLazySingle()进来,后续直接报错
                flag = false;
            }else {
                throw new RuntimeException("不要试图用反射破坏单例");
            }
        }
    }

    public static volatile LazySingle lazySingle = null;

    public static synchronized LazySingle getLazySingle(){
        if (lazySingle == null){
            synchronized (LazySingle.class){
                if (lazySingle == null){
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }
}

但是有问题存在,反射难道不能破坏属性吗?

单例继续破坏

public class ReflectionTest {
    public static void main(String[] args) throws Exception {
        Class<? extends LazySingle> aClass1 = LazySingle.class;
        Constructor<? extends LazySingle> declaredConstructor = aClass1.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazySingle lazySingle = declaredConstructor.newInstance(null);
        System.out.println(lazySingle);

        Class<? extends LazySingle> aClass2 = LazySingle.class;
        Constructor<? extends LazySingle> declaredConstructor1 = aClass2.getDeclaredConstructor(null);
        declaredConstructor1.setAccessible(true);
        Field flag = aClass2.getDeclaredField("flag");
        flag.setAccessible(true);
        flag.set(aClass2,true);
        LazySingle lazySingle1 = declaredConstructor1.newInstance(null);
        System.out.println(lazySingle1);
    }
}

执行结果,明显两个值不相等。

com.test.LazySingle@6f75e721
com.test.LazySingle@782830e

那又该如何解决呢?反射初始化方法newInstance源码给出如下提示

4.2.3、枚举防止反射破坏

新建枚举类EnumSingle

public enum EnumSingle {
    INSTANCE;
    
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

编译完枚举类可以直接反编译看出,枚举类其实就是一个Java类

为了测试上诉情况,直接使用反射创建枚举,看是否为出现反射定义的枚举错误

先通过idea自带反编译工具查看枚举类型的构造函数,如下可以看出是无参构造

class Test{
    public static void main(String[] args) throws Exception {
        Class<EnumSingle> enumSingleClass = EnumSingle.class;
        Constructor<EnumSingle> declaredConstructor = enumSingleClass.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingle enumSingle = declaredConstructor.newInstance(null);
        System.out.println(enumSingle);
    }
}

执行结果如下

上诉报错原因是构造方法出错,但明明idea反编译工具显示的是无参构造,为什么会报这样的错呢?

重新使用市面上流行的jad反编译工具在cmd窗口执行jad -sjava EnumSingle.class,结果如下

public final class EnumSingle extends Enum
{

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

    public static EnumSingle valueOf(String name)
    {
        return (EnumSingle)Enum.valueOf(com/test/EnumSingle, name);
    }

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

    public EnumSingle getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingle INSTANCE;
    private static final EnumSingle $VALUES[];

    static 
    {
        INSTANCE = new EnumSingle("INSTANCE", 0);
        $VALUES = (new EnumSingle[] {
            INSTANCE
        });
    }
}

通过jad编译后的文件直接可以看出,此枚举类型的构造方法并非无参构造,而是private EnumSingle(String s, int i)

修改代码如下

class Test{
    public static void main(String[] args) throws Exception {
        Class<EnumSingle> enumSingleClass = EnumSingle.class;
        // private EnumSingle(String s, int i)
        Constructor<EnumSingle> declaredConstructor = enumSingleClass.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle enumSingle = declaredConstructor.newInstance(null);
        System.out.println(enumSingle);
    }
}

执行结果,达到目的

4.3、序列化

4.3.1、代码实现

public class SerializableSingle {
    public static void main(String[] args) throws Exception {
        // Write to file
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testFile"));
        oos.writeObject(LazySingle.getLazySingle());
        // Read from file
        File file = new File("testFile");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        LazySingle newInstance = (LazySingle) ois.readObject();
        System.out.println(newInstance == LazySingle.getLazySingle());
    }
}

4.3.2、源码分析

1、因为存在不同的对象,那么从如下代码开始分析

LazySingle newInstance = (LazySingle) ois.readObject();

2、进入源码readObject方法

public final Object readObject()
        throws IOException, ClassNotFoundException{
        if (enableOverride) {
            return readObjectOverride();
        }
        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            // 生成对象的方法
            Object obj = readObject0(false);
            // 此处省略部分代码
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

3、进入方法readObject0

private Object readObject0(boolean unshared) throws IOException {
        // 此处省略代码
        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                // 此处省略代码
                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));
                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

4、进入方法readOrdinaryObject()

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        // 此处省略代码
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
            // 如果实例化对象有readResolve方法,hasReadResolveMethod返回true
            // 此方法体主要是取出readResolve方法的值rep将obj引用指向readResolve方法的返回值rep
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

从上面源码可知,实例化对象需要存在一个readResolve方法即可,但是怎么确定参数和返回值呢?

由ObjectStreamClass类构造方法可以得出

readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);

private static Method getInheritableMethod(Class<?> cl, String name,
                                               Class<?>[] argTypes,
                                               Class<?> returnType){}

综上实例化对象加入方法readResolve如下所示。

// 该方法并非被重写的方法
private Object readResolve(){
    return lazySingle;
}

运行代码,返回true成功。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码