前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >单例模式的运用

单例模式的运用

原创
作者头像
忆遂愿
发布2024-08-01 11:26:27
740
发布2024-08-01 11:26:27

一、介绍

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

单例设计模式分类两种:

  • 饿汉式:类加载就会导致该单实例对象被创建。
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。

二、饿汉式

2.1 静态变量方式

该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是**随着类的加载而创建的**。如果该对象足够大的话,而一直没有使用就会造成**内存的浪费**。

代码语言:java
复制
/\*\*

 \* 静态变量创建类的对象

 \*/

public class Singleton {

    // 私有类的无参构造

    private Singleton() {}

    // 给本类创建一个新的对象,并用私有化无法访问,使用静态关键字static来修饰

    private static Singleton instance = new Singleton();

    // 提供一个公共的访问方式,由于外界无法创建Singleton对象,无法调用非静态的方法,所以设计为静态的方法

    public static Singleton getInstance(){

        return instance;

    }

}



public static void main(String[] args) {

          // 重复调用Singletion类的getInstance方法

        Singleton instance1 = Singleton.getInstance();

        Singleton instance2 = Singleton.getInstance();

        // 判断获取的两个对象是否为同一个对象,即申请的内存地址是否相同,来证明单例模式

        System.out.println(instance1 == instance2);//true

}

2.2 静态代码块方式

该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在**静态代码块**中,也是**对着类的加载而创建**。所以和饿汉式的静态变量方式基本上一样,当然该方式也存在**内存浪费**问题。

代码语言:java
复制
/\*\*

 \* 静态变量创建类的对象

 \*/

public class Singleton {

    // 私有的构造方法

    private Singleton(){}

    // 给本类声明Singleton类型的变量,并用私有化无法访问,使用静态关键字static来修饰

    private static Singleton instance;//null

    // 在静态代码块中进行赋值,创建Singleton的对象

    static{

        instance = new Singleton();

    }

    // 提供一个公共的访问方式,由于外界无法创建Singleton对象,无法调用非静态的方法,所以设计为静态的方法

    public static Singleton getInstance() {

        return instance;

    }

}



public static void main(String[] args) {

        // 重复调用Singletion类的getInstance方法

        Singleton instance1 = Singleton.getInstance();

        Singleton instance2 = Singleton.getInstance();

        // 判断获取的两个对象是否为同一个对象,即申请的内存地址是否相同,来证明单例模式

        System.out.println(instance1 == instance2);//true

}

2.3 枚举方式

枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是**线程安全**的,并且**只会加载一次**,枚举类型是所用单例实现中**唯一一种不会被破坏的**单例实现模式。

代码语言:java
复制
/\*

\* 枚举方式

\* 枚举方式属于恶汉式方式

\*\*/

public enum Singleton {

    INSTANCE;

}



public static void main(String[] args) {

    // 重复调用Singletion枚举类的INSTANCE

    Singleton instance1 = Singleton.INSTANCE;

    Singleton instance2 = Singleton.INSTANCE;

    System.out.println(instance1 == instance2);

}

三、懒汉式

3.1 线程不安全方式

该方式在成员位置声明Singleton类型的静态变量,当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了**懒加载**的效果。但是,在**多线程环境,出现线程安全问题**。

代码语言:java
复制
/\*\*

 \* 线程不安全

 \*/ 

public class Singleton {

    // 私有的构造方法

    private Singleton(){}

    // 给本类声明Singleton类型的变量,并用私有化无法访问,使用静态关键字static来修饰

    private static Singleton instance;//null

    // 提供一个公共的访问方式,由于外界无法创建Singleton对象,无法调用非静态的方法,所以设计为静态的方法

    public static Singleton getInstance() {

        // 判断instance是否为null,如果为null,说明还没有创建Singleton类的对象

        // 如果不为空,说明已将创建过,返回对象即可

        if(instance == null){

            instance = new Singleton();

        }

        return instance;

    }

}



public static void main(String[] args) {

        // 重复调用Singletion类的getInstance方法

        Singleton instance1 = Singleton.getInstance();

        Singleton instance2 = Singleton.getInstance();

        // 判断获取的两个对象是否为同一个对象,即申请的内存地址是否相同,来证明单例模式

        System.out.println(instance1 == instance2);//true

}

3.2 线程安全方式

该方式也实现了懒加载效果,同时又**解决了线程安全问题**。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在该问题了。

代码语言:java
复制
/\*\*

 \* 线程安全

 \*/ 

public class Singleton {

    // 私有的构造方法

    private Singleton(){}

    // 给本类声明Singleton类型的变量,并用私有化无法访问,使用静态关键字static来修饰

    private static Singleton instance;//null

    // 提供一个公共的访问方式,由于外界无法创建Singleton对象,无法调用非静态的方法,所以设计为静态的方法

    public static synchronized Singleton getInstance() {

        // 判断instance是否为null,如果为null,说明还没有创建Singleton类的对象

        // 如果不为空,说明已将创建过,返回对象即可

        if(instance == null){

            // 加锁前,线程1、2都进入到该位置,此时就会出现创建多个对象的情况

            // 加锁后,线程1进入,线程2会在外面等待线程1运行完毕,释放锁后才继续执行

            instance = new Singleton();

        }

        return instance;

    }

}



public static void main(String[] args) {

        // 重复调用Singletion类的getInstance方法

        Singleton instance1 = Singleton.getInstance();

        Singleton instance2 = Singleton.getInstance();

        // 判断获取的两个对象是否为同一个对象,即申请的内存地址是否相同,来证明单例模式

        System.out.println(instance1 == instance2);//true

}

3.3 双重检查锁方式

该方式对懒汉模式中加锁的问题做出变化,对于 getInstance() 方法来说,绝大部分的操作都是**读操作**,读操作是**线程安全**的,所以没必让每个线程必须持有锁才能调用该方法,我们需要**调整加锁的时机**。

在**多线程的情况**下,可能会出现**空指针问题**,出现问题的原因是JVM在实例化对象的时候会进行**优化和指令重排序**操作。要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证**可见性和有序性**。

代码语言:java
复制
/\*

\* 双重检查锁

\* 添加 volatile关键字之后的双重检查锁模式是一种比较好的单例

\* 实现模式,能够保证在多线程的情况下线程安全也不会有性能问题

\*\*/

public class Singleton {

    // 私有的构造方法

    private Singleton(){}

    // 给本类声明Singleton类型的变量,并用私有化无法访问,使用静态关键字static来修饰

    private static volatile Singleton instance;//null

    // 提供一个公共的访问方式,由于外界无法创建Singleton对象,无法调用非静态的方法,所以设计为静态的方法

    public static Singleton getInstance() {

        // 第一次判断,判断instance是否为null,如果为null,说明还没有创建Singleton类的对象,不需要抢占锁

        // 如果不为空,说明已将创建过,返回对象即可,读操作直接返回,没有加锁

        if(instance == null){

            // 第二次判断,写操作需要加锁

            synchronized (Singleton.class) {

                if (instance == null) {

                    instance = new Singleton();

                }

            }

        }

        return instance;

    }

}



public static void main(String[] args) {

        // 重复调用Singletion类的getInstance方法

        Singleton instance1 = Singleton.getInstance();

        Singleton instance2 = Singleton.getInstance();

        // 判断获取的两个对象是否为同一个对象,即申请的内存地址是否相同,来证明单例模式

        System.out.println(instance1 == instance2);//true

}

3.4 静态内部类方式

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

静态内部类方式在**没有加任何锁**的情况下,保证**线程安全**,并且**没有任何性能影响和空间的浪费**。

代码语言:java
复制
/\*

\* 静态内部类方式

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

\*\*/

public class Singleton {

    // 私有的构造方法

    private Singleton(){}

    //定义一个静态内部类

    private static class SingletonHolder {

        //在内部类中声明Singleton类型的变量并初始化外部类的对象,并用私有化无法访问,使用静态关键字static和final来修饰

        private static final Singleton INSTANCE = new Singleton();

    }

    // 提供一个公共的访问方式,由于外界无法创建Singleton对象,无法调用非静态的方法,所以设计为静态的方法

    public static Singleton getInstance() {

        // 内部类直接调用对象

        return SingletonHolder.INSTANCE;

    }

}



public static void main(String[] args) {

        // 重复调用Singletion类的getInstance方法

        Singleton instance1 = Singleton.getInstance();

        Singleton instance2 = Singleton.getInstance();

        // 判断获取的两个对象是否为同一个对象,即申请的内存地址是否相同,来证明单例模式

        System.out.println(instance1 == instance2);//true

}

四、破坏单例模式

4.1 序列化破坏

代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式。

对象里面的值是一样的,只不过对象的引用不一样,相当于是两个对象,涉及到了深拷贝

代码语言:java
复制
/\*\*

 \* 静态内部类方式的单列模式

 \*/

public class Singleton implements Serializable {

    // 私有的构造方法

    private Singleton(){}

    //定义一个静态内部类

    private static class SingletonHolder {

        //在内部类中声明Singleton类型的变量并初始化外部类的对象,并用私有化无法访问,使用静态关键字static和final来修饰

        private static final Singleton INSTANCE = new Singleton();

    }

    // 提供一个公共的访问方式,由于外界无法创建Singleton对象,无法调用非静态的方法,所以设计为静态的方法

    public static Singleton getInstance() {

        // 内部类直接调用对象

        return SingletonHolder.INSTANCE;

    }

}



public class Client {

    public static void main(String[] args) throws Exception {

//        writeObjectToFile();

        Singleton s1 = readObjectFromFile();

        Singleton s2 = readObjectFromFile();

        System.out.println(s1 == s2);

    }

    // 使用序列化向文件中写数据(对象)

    private static void writeObjectToFile() throws IOException {

        // 获取Singleton对象

        Singleton instance = Singleton.getInstance();

        // 创建对象输出流对象

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\a.txt"));

        // 将对象写入

        objectOutputStream.writeObject(instance);

        // 释放资源

        objectOutputStream.close();

    }

    // 使用反序列化读取文件的数据(对象)

    private static Singleton readObjectFromFile() throws Exception {

        // 创建对象输入流对象

        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\a.txt"));

        // 读取对象

        Singleton instance = (Singleton) objectInputStream.readObject();

        System.out.println(instance);

        // 释放资源

        objectInputStream.close();

        return instance;

    }

}

4.2 序列化破坏解决办法

想要解决序列化、反序列化对单例模式的破坏,我们需要在Singleton类中添加readResolve()方法,在其被反序列化时调用,返回这个方法的值(对象)。 **原理**是在输入流对象的readObject方法的底层实现中,如果发现输入的是对象类型, 则会调用readOrdinaryObject方法,该方法中会自动执行hasReadResolveMethod方法,来判断类中是否有readResolve方法,如果有,则会去执行invokeReadResolve方法来获取类中专门提供的实例,从而解决反序列化破坏单例模式的问题。

代码语言:java
复制
// 当进行反序列化时,会自动调用该方法,将该方法的返回值直接返回

public Object readResolve(){

    return SingletonHolder.INSTANCE;

}

4.3 反射破坏

代码运行结果是false,表明反射已经破坏了单例设计模式。

代码语言:java
复制
/\*\*

 \* 静态内部类方式的单列模式

 \*/

public class Singleton implements Serializable {

    // 私有的构造方法

    private Singleton(){}

    //定义一个静态内部类

    private static class SingletonHolder {

        //在内部类中声明Singleton类型的变量并初始化外部类的对象,并用私有化无法访问,使用静态关键字static和final来修饰

        private static final Singleton INSTANCE = new Singleton();

    }

    // 提供一个公共的访问方式,由于外界无法创建Singleton对象,无法调用非静态的方法,所以设计为静态的方法

    public static Singleton getInstance() {

        // 内部类直接调用对象

        return SingletonHolder.INSTANCE;

    }

}



public class Client {

    public static void main(String[] args) throws Exception {

        // 获取Singleton字节码对象

        Class<Singleton> singletonClass = Singleton.class;

        // 获取无参构造方法对象

        Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();

        // 取消访问检查

        declaredConstructor.setAccessible(true);

        // 创建Singleton对象

        Singleton s1 = (Singleton) declaredConstructor.newInstance();

        Singleton s2 = (Singleton) declaredConstructor.newInstance();

        // 结果返回false,说明反射破坏了单例模式

        System.out.println(s1 == s2);

    }

}

4.4 反射破坏解决办法

当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此次操作。

代码语言:java
复制
/\*\*

 \* 静态内部类方式的单列模式

 \*/

public class Singleton {

    // 定义一个flag判断是否已经创建过对象

    private static boolean flag = false;

    // 给私有构造方法添加锁,预防线程安全问题

    private Singleton(){

        synchronized(Singleton.class){

            // flag为false则跳过,直接正常创建,若flag为true,说明已经创建过,抛出异常

            if(flag){

                throw new RuntimeException("不能创建多个对象");

            }

            // 第一次创建后,应该将flag设置为true

            flag = true;

        }

    }

    //定义一个静态内部类

    private static class SingletonHolder {

        //在内部类中声明Singleton类型的变量并初始化外部类的对象,并用私有化无法访问,使用静态关键字static和final来修饰

        private static final Singleton INSTANCE = new Singleton();

    }

    // 提供一个公共的访问方式,由于外界无法创建Singleton对象,无法调用非静态的方法,所以设计为静态的方法

    public static Singleton getInstance() {

        // 内部类直接调用对象

        return SingletonHolder.INSTANCE;

    }

}

记录每一个学习瞬间

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、介绍
  • 二、饿汉式
    • 2.1 静态变量方式
      • 2.2 静态代码块方式
        • 2.3 枚举方式
        • 三、懒汉式
          • 3.1 线程不安全方式
            • 3.2 线程安全方式
              • 3.3 双重检查锁方式
                • 3.4 静态内部类方式
                • 四、破坏单例模式
                  • 4.1 序列化破坏
                    • 4.2 序列化破坏解决办法
                      • 4.3 反射破坏
                        • 4.4 反射破坏解决办法
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档