首页 前端知识 单例模式(饿汉式、懒汉式)详解

单例模式(饿汉式、懒汉式)详解

2024-06-16 09:06:01 前端知识 前端哥 998 324 我要收藏

对于单例模式,很多小伙伴都不陌生吧,单例模式是设计模式中最简单的设计模式之一,大家或多或少有写过单例模式的代码,但是时间一久,又忘记了怎么写,下面,跟着小编一起,学习或回顾一下单例模式吧

1. 什么是单例模式

单例模式:是指一个类只会创建一次对象的设计模式,属于设计模式创建者模式中的一种。这个类提供了一种唯一访问该对象的方式,也就是说,这个类的实现只会在内存中出现一次。这样子的好处是防止频繁的创建对象导致内存资源浪费。

2. 单例模式的两种形式

饿汉式:在类被加载时就会创建该类的实例对象

懒汉式:在类被加载时不回创建该类的实例对象,在首次要使用该实例时才会创建

3. 单例模式的特点

1.单例类只会有一个实例

2.单例类的实例由该类自己提供对外访问的方法

3.单例类的构造函数必须是私有

4.饿汉式

饿汉式单例模式是指在类被加载时就会创建该类的实例对象

饿汉式有两种实现方法

  1. 用静态的成员变量创建本类对象
  2. 在静态代码块中创建本类对象

接下来,我们分别介绍这两种方式:

4.1 静态成员变量创建对象

静态成员变量实现单例模式,需要声明一个私有的静态成员变量并赋值,然后对外暴露出可以公共访问的方式,记得一定要把构造方法私有化,这样子,就可以创建出一个单例对象,该对象是在类被加载时就会被创建。

/**
 * 单例模式 饿汉式 1. 静态成员变量方式
 */
public class Singleton1 {
    // 1.私有构造方法
    private Singleton1() {}

    // 2. 私有的静态成员变量
    private static Singleton1 instance = new Singleton1();

    // 3.对外暴露公共访问方式
    public static Singleton1 getInstance() {
        return instance;
    }
}
4.2 静态代码块创建对象

静态代码块中创建对象,顾名思义,就是在静态代码块中给对象赋值,首先,我们一样要将构造方法私有化,然后在成员变量中声明一个该类的对象,在静态代码块中给对象赋值,最后,只有对外暴露出公共的访问方式即可。

5. 懒汉式

懒汉式单例模式是指在类被加载时不回创建该类的实例对象,在首次要使用该实例时才会创建

懒汉式也有两种实现方式:

  1. 判断是否首次使用,是则创建对象,不是则不创建。
  2. 利用静态内部类的特性创建对象

接下来,我们就进入这两种方式的学习:

5.1 线程不安全创建对象

懒汉式最容易让人想到的创建方式是构造方法私有化,声明成员对象,然后在对外暴露访问方式时做判断。判断成员对象是否被赋值了,如果被赋值,被不创建直接返回,如果没被赋值,则创建对象再返回。那么,代码的实现就如下: 

/**
 * 单例模式 懒汉式 线程不安全
 */
public class Singleton3 {
    // 1.私有构造方法
    private Singleton3() {}

    // 2.声明成员变量
    private static Singleton3 instanse;

    // 3.对外提供
    public static Singleton3 getInstance() {

       if (instanse == null) {
           instanse = new Singleton3();
       }
       return instanse;
    }
}

实际上这种做法是存在问题的,从上面代码我们可以看出该方式在成员位置声明Singleton3类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton3类的对象的时候才创建Singleton3类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。

5.2 线程安全创建对象

既然上面的懒汉式单例模式会出现线程安全问题,我们能想到最直观的方式解决线程安全的问题就是加锁,那么改进后的代码如下:

/**
 * 单例模式 懒汉式 线程安全
 */
public class Singleton3 {
    // 1.私有构造方法
    private Singleton3() {}

    // 2.声明成员变量
    private static Singleton3 instanse;

    // 3.对外提供
    public static synchronized Singleton3 getInstance() {
       if (instanse == null) {
           instanse = new Singleton3();
       }
       return instanse;
    }
}

可以看到,我们在getInstance方法上面加了一个关键字synchronized,这样子,我们就解决了线程安全问题。但是,这就真的是我们最完美的解决方案吗?显然不是,从上面的代码可以看出,我们在每次获取对象的时候都需要先去获取锁,并发性能非常的差,而且,我们只是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

接下来,我们就想想办法怎么去优化性能,既然初始化完成后就不需要创建对象了,那我们可以只在初始化对象的时候加锁,实例化完对象后无需加锁。

5.3 双重检测法

我们只要在创建对象时加锁,在不创建对象时不需要加锁,我们可以在加锁前做一个判断,如果对象被创建了则直接返回,如果对象没创建则进到加锁区域,在锁内在进行判断对象是否真的为空,为空则创建对象。这样子,我们第一个判断是保证了性能,第二个判断保证线程安全。实际上,在多线程的情况下,可能会出现问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。我们可以使用volatile关键字修饰的变量,可以保证其指令的有序性和可见性 。最终我们的代码是实现如下:

/**
 * 单例模式 懒汉式 双重检测法
 */
public class Singleton3 {
    // 1.私有构造方法
    private Singleton3() {}

    // 2.声明成员变量
    private static volatile Singleton3 instanse;

    // 3.对外提供
    public static Singleton3 getInstance() {
        // 双重检测
       if(instanse == null) {    // 保证性能
           synchronized (Singleton3.class) {
               if (instanse == null) {   // 保证线程安全
                   instanse = new Singleton3();
               }
           }
       }
       return instanse;
    }
}
5.4 静态内部类创建对象

静态内部类的特性:静态内部类单例模式中实例由内部类创建,由于虚拟机在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性或方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

静态内部类的特性完全符合懒汉式单例模式的特点,在加载外部类时不会创建静态内部类,在静态内部类中的属性被调用时才会创建,如果静态内部类的属性被static修饰,只会实例化一次。那么,我们的代码实现可以如下:

package com.coderedma.shejimoshi.creator.singleton.demo4;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * 单例模式 懒汉式 静态内部类
 */
public class Singleton4 {
  
    // 1.私有构造方法
    private Singleton4() {}

    // 2. 声明一个静态内部类
    private static class SingletonHolder {
        private static final Singleton4 INSTANCE = new Singleton4();
    }

    // 3.对外提供访问方式
    public static Singleton4 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

第一次加载Singleton4类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机才会加载SingletonHolder并初始化INSTANCE。这样不仅能确保线程安全,也能保证 Singleton4 类的唯一性。

实际上,静态内部类单例模式是一种优秀的单例模式,是在项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

6.破坏单例模式

什么叫做破坏单例模式呢?从单例模式的定义入手,单例模式是一个类只会创建一个对象,即一个类的实例化只会在内存中出现一次。那么,如果一个类可以创建多个对象,不就是破坏了单例模式吗?

破坏单例模式:一个类可以创建多个对象

破坏单例模式有两种方式  反序列化和反射

6.1 反序列化破坏单例模式

我们可以通过反序列化破坏单例模式。下面,就是验证我们的结论,反序列化是否可以破坏单例模式。

我们知道,类在实现了Serializable接口就可以实现对象的反序列化。

public class Singleton4 implements Serializable

我们写一个方法将对象序列化

 /**
     * 对象写入aaa.txt中
     * @throws Exception
     */
    public void writeObject() throws Exception {
        Singleton4 instance = Singleton4.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\凉尘\\Documents\\java\\javashejimoshi\\aaa.txt"));
        oos.writeObject(instance);
        oos.close();
    }

我们创建一个Singleton4类的对象,将这个对象通过ObjectOutputStream类序列化到aaa.txt这个文件中去。

然后,我们再写一个方法将对象反序列化

/**
     * 从aaa.txt中读取对象
     * @return
     * @throws Exception
     */
    public Singleton4 readObject() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\凉尘\\Documents\\java\\javashejimoshi\\aaa.txt"));
        Singleton4 instance = (Singleton4) ois.readObject();
        ois.close();
        return instance;
    }

我们通过ObjectInputStream类读取aaa.txt总的对象,将它转为Singleton4类型。然后,我们写一个test方法,验证我们的结论,反序列化是否可以破坏单例模式。

// 测试破坏单例模式 反序列化
    @Test
    public void testAttactSingleton() throws Exception {
        // 写入对象
        writeObject();
        // 读取对象
        Singleton4 instance1 = readObject();
        Singleton4 instance2 = readObject();
        // 判断两个对象是否是同一个对象
        System.out.println(instance1 == instance2);
    }

我们在方法中先通过writeObject方法将对象写入到aaa.txt中,再通过readObject方法从aaa.txt中读取对象,我们读了两次对象,然后判断两个对象是否是同一个,在Java中,== 比较的是两个对象的地址是否相同,如果相同,返回true,如果不相同,返回false。

运行代码,得到如下结果:

false

返回false,则表示两个对象在内存中不是同一块地址。所以单例模式被破坏。我们的结论成立,反序列化可以破坏单例模式。

6.2 反射破坏单例模式

我们可以通过反射破坏单例模式。

我们写一个test方法,看看反射是否真的可以破坏单例模式。

// 测试破坏单例模式 反射
    @Test
    public void testAttactSingleton2() throws Exception {
        // 获取类的字节码对象
        Class<Singleton4> clazz = Singleton4.class;
        // 获取构造函数
        Constructor<Singleton4> ctor = clazz.getDeclaredConstructor();
        // 不检查权限
        ctor.setAccessible(true);
        // 创建对象
        Singleton4 instance1 = ctor.newInstance();
        Singleton4 instance2 = ctor.newInstance();
        // 判断两个对象是否是同一个对象
        System.out.println(instance1 == instance2);
    }

我们知道,反射首先要获取类的字节码对象,然后通过字节码对象获取类的构造函数、成员方法等。我们这里是获取类的构造函数,由于,我们的构造函数是设置了私有,所以需要让虚拟机不检查权限,然后,我们可以拿着类的构造方法去实例化对象,实例出两个对象做比较。

false

比较的结果是false,表示两个对象在内存中不是同一块地址。所以,我们的反射同样可以破坏单例模式。

既然我们的反序列化和反射都可以破坏单例模式,那么有没有什么解决方案让反序列化和反射破坏不了单例模式呢,答案是有的,至于,怎么解决问题,就交给小伙伴们自己去思考了。

7. 总结

(1)单例模式是一个类只会创建一个对象

(2)单例模式有两种形式: 饿汉式、懒汉式

(3)饿汉式单例模式是类在加载时就会创建对象,懒汉式单例模式是类在加载时不会创建对象,只有在首次使用时才会创建对象

(4)饿汉式有两种实现方法:静态成员变量创建对象静态代码块创建对象

(5)懒汉式有两种实现方法:双重检测法创建对象静态内部类创建对象

(6)破坏单例模式有两种方法:反序列化和反射

转载请注明出处或者链接地址:https://www.qianduange.cn//article/12371.html
评论
发布的文章

JQuery中的load()、$

2024-05-10 08:05:15

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!