设计模式-单例模式的完整代码示例及测试验证

admin2024-07-10  3

单例模式

什么是单例模式?

单例模式(Singleton Pattern)用于确保某个类在整个应用程序中只有一个实例,并提供一个全局访问点来获取该实例。

优缺点

  • 优点
  • 缺点

单例模式实现方式

通常有如下5种写法:

/**
 * @Author: javafa
 * @Date: 2024/7/9 13:47
 * @Description: 饿汉式 单例模式
 */
public class LazyMan {
    private static LazyMan lazyMan;

    public LazyMan(){
        System.out.println(Thread.currentThread().getName());
    }

    /**
     * @Author: javafa
     * @Description: 懒汉式 单例模式
     * 优点:
     * 第一次调用才初始化,避免内存浪费。
     * 缺点:
     * 多线程调用时,并不能保证提供唯一实例,通过无参构造发现,会多次创建实例
     * 基于此缺点,添加双重检验锁进行完善
     * @Date: 2024/7/9 13:52
     * @Param:
     * @return: com.fivemillion.algorithm.designpatterns.singleton.LazyMan
     * @see SingletonTest#useLayManTest()
    **/
    public static LazyMan getInstance(){
        if (lazyMan == null) {
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }

}

@Data
public class Hungry implements Serializable {

    private static final long serialVersionUID = 1L;

    private static boolean instanceCreated = false;

    private Hungry(){
        /*//防止单例被破坏
        if (instanceCreated) {
            throw new RuntimeException("请使用 Hungry.getInstance() 方法获取一个单例实例");
        }
        instanceCreated = true;
        */
        System.out.println(Thread.currentThread().getName() + " is creating an instance.");
    }

    private static final Hungry hungry = new Hungry();

    /**
     * @Author: javafa
     * @Description: 在饿汉式单例模式中,单例实例在类加载时就被创建,并且只会创建一次
     * v优点:
     * 1、简单直观,易于实现。
     * 2、线程安全,因为实例在类加载时就已经创建。
     *
     * 缺点:
     * 1、可能会造成资源浪费,因为实例在类加载时就创建了,即使从未使用它。
     * 基于此缺点,引入了懒汉式单例模式
     *
     * @Date: 2024/7/9 13:37
     * @Param:
     * @return: com.fivemillion.algorithm.designpatterns.singleton.Hungry
     * @see SingletonTest#useHungryTest()
    **/
    public static Hungry getInstance(){
        return hungry;
    }
    public Object readResolve() throws ObjectStreamException {
        return hungry;
    }

    private int id;
    private String name;
    private int age;
    private String address;
}

public class StaticInner {
    private StaticInner(){
        System.out.println(Thread.currentThread().getName());
    }

    public static class InnerSingleton{
        private static StaticInner staticInner = new StaticInner();
    }

    /**
     * @Author: javafa
     * @Description:
     * 优点:
     * 支持多线程,是线程安全的,由于jvm的classloder机制会确保在加载内部类时,只会有一个线程能够初始化 staticInner,从而保证了单例的线程安全性
     * 支持懒加载,静态内部类的实例 staticInner 只有在 getInstance() 方法首次被调用时才会被初始化。这种懒加载的方式确保了单例实例在首次使用时才被创建
     * 性能高,代码简洁,没有使用synchronized 同步锁
     *
     * @Date: 2024/7/9 16:19
     * @Param:
     * @return: com.fivemillion.algorithm.designpatterns.singleton.StaticInner
     * @see SingletonTest#useStaticInnerTest()
    **/
    public static StaticInner getInstance(){
        return  InnerSingleton.staticInner;
    }
}

public class LazyDoubleCheckMan {
    private static LazyDoubleCheckMan lazyMan;

    public LazyDoubleCheckMan(){
        System.out.println(Thread.currentThread().getName());
    }

    /**
     * @Author: javafa
     * @Description: 饿汉式-双重检验锁(double check lock)(DCL) 单例模式
     * 优点:
     * 懒汉模式的升级版,保证全局唯一实例
     * 缺点:
     * 由于存在cpu指令重排,可能导致创建的对象为null被返回
     * 基于此缺点需要使用volatile 进行优化
     *
     * @Date: 2024/7/9 13:52
     * @Param:
     * @return: com.fivemillion.algorithm.designpatterns.singleton.LazyMan
     * @see SingletonTest#useLayManTest()
    **/
    public static LazyDoubleCheckMan getInstance(){
        if (lazyMan == null) { //第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
            synchronized(LazyDoubleCheckMan.class){//第一层锁,保证只有一个线程进入
                //双重检查,防止多个线程同时进入第一层检查(因单例模式只允许存在一个对象,故在创建对象之前无引用指向对象,所有线程均可进入第一层检查)
                //当某一线程获得锁创建一个LazyMan对象时,即已有引用指向对象,lazyMan不为空,从而保证只会创建一个对象
                //假设没有第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
                if (lazyMan == null) {//第二层检查
                    lazyMan = new LazyDoubleCheckMan();//这行代码存在的问题,不能保证原子性
                    //对象的创建并不是一个简单的原子操作,而是由多个步骤组成:(1)在堆上开辟空间;(2)属性初始化;(3)引用指向对象。
                    //由于 JVM 的指令重排优化,步骤 2 和步骤 3 可能会被重排,从而导致另外一个线程在步骤 3 完成而步骤 2 未完成时,看到一个不完整的对象
                    //假设以上三个内容为三条单独指令,因指令重排可能会导致执行顺序为1->3->2(正常为1->2->3),
                    // 当单例模式中存在普通变量需要在构造方法中进行初始化操作时,单线程情况下,顺序重排没有影响;
                    // 但在多线程情况下,假如线程1执行lazyMan = new LazyMan()语句时先1再3,由于系统调度线程2的原因没来得及执行步骤2,
                    // 但此时已有引用指向对象也就是lazyMan!=null,故线程2在第一次检查时不满足条件直接返回lazyMan,但此时lazyMan为null
                }
            }
        }
        return lazyMan;
    }

}
public class LazyDoubleCheckImproveMan {
    //使用 volatile 修饰
    //volatile作用:保证有序性、可见性。
    private static volatile LazyDoubleCheckImproveMan lazyMan;

    public LazyDoubleCheckImproveMan(){
        System.out.println(Thread.currentThread().getName());
    }

    /**
     * @Author: javafa
     * @Description: 饿汉式-双重检验锁(double check lock)(DCL) 单例模式
     * 优点:
     * 懒汉模式的升级版,保证全局唯一实例,且使用volatite修饰,禁止了cpu指令重排,保证了多线程安全
     * 缺点:
     * volatile 会强制cpu即使把修改的值立即被更新到主存,且使用synchronized同步加锁,性能较低
     *
     * @Date: 2024/7/9 13:52
     * @Param:
     * @return: com.fivemillion.algorithm.designpatterns.singleton.LazyMan
     * @see SingletonTest#useLayManTest()
    **/
    public static LazyDoubleCheckImproveMan getInstance(){
        if (lazyMan == null) { //第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
            synchronized(LazyDoubleCheckImproveMan.class){//第一层锁,保证只有一个线程进入
                //双重检查,防止多个线程同时进入第一层检查(因单例模式只允许存在一个对象,故在创建对象之前无引用指向对象,所有线程均可进入第一层检查)
                //当某一线程获得锁创建一个LazyMan对象时,即已有引用指向对象,lazyMan不为空,从而保证只会创建一个对象
                //假设没有第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
                if (lazyMan == null) {//第二层检查
                    lazyMan = new LazyDoubleCheckImproveMan();
                    //对象的创建并不是一个简单的原子操作,而是由多个步骤组成:(1)在堆上开辟空间;(2)属性初始化;(3)引用指向对象。
                    //由于 JVM 的指令重排优化,步骤 2 和步骤 3 可能会被重排,从而导致另外一个线程在步骤 3 完成而步骤 2 未完成时,看到一个不完整的对象
                    //由于lazyMan变量声明为 volatile,就指示 JVM,修改的值立即被更新到主存,使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行
                }
            }
        }
        return lazyMan;
    }
}

@Data
public class Student implements Serializable {
    private static final long serialVersionUID = 1L;

    // 私有构造方法,防止外部实例化
    private Student() {
        System.out.println(Thread.currentThread().getName());
    }

    private int id;
    private String name;
    private int age;
    private String address;
    private String gradeNo;
    private int result;



    /**
     * 枚举类型是线程安全的,并且只会装载一次
     */
    private enum SingletonStudent{
        INSTANCE;

        private final Student student;

        SingletonStudent(){
            student = new Student();
        }

        private Student getInstance(){
            return student;
        }
    }

    public static Student getInstance(){
        return SingletonStudent.INSTANCE.getInstance();
    }

    /**
     * @Author: javafa
     * @Description:
     * 如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例
     * 所以要么不实现序列化接口,
     * 如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象
     * @Date: 2024/7/10 11:08
     * @Param:
     * @return: java.lang.Object
     * @see SerializationTest#serializaStudentTest()
    **/
    private Object readResolve() {
        return SingletonStudent.INSTANCE.getInstance();
    }
}

破坏单例模式的方法及解决办法

  1. 除枚举方式外, 其他方法都会通过 反射的方式破坏单例,反射是通过调用构造方法生成新的对象
  • 反射破坏代码示例
public class ReflectionTest {

    @Test
    public void reflectStudentTest(){
        try {
            Class<?> clazz = Class.forName("Student$SingletonStudent");
            Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
            constructor.setAccessible(true);
            Object instance = constructor.newInstance("INSTANCE", 0);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @Test
    public void reflectHungryTest() throws Exception {
        // 获取类的显式构造器
        Constructor<Hungry> constructor = Hungry.class.getDeclaredConstructor();
        // 将访问权限设为 true,从而可以访问类的私有构造器
        constructor.setAccessible(true);
        // 利用反射构造一个新对象
        Hungry instance1 = constructor.newInstance();
        // 再通过正常的单例模式获取单例对象
        Hungry instance2 = Hungry.getInstance();
        // 比较两个对象是不是同一个对象
        System.out.println(instance1 == instance2); // 打印结果为 false

        System.out.println(instance1.hashCode() == instance2.hashCode());// 打印结果为true
        //hashCode()方法返回的哈希码值理论上应该能够唯一地标识一个对象,
        // 但实际中可能会发生哈希冲突,即两个不同的对象可能具有相同的哈希码值,当测试为instance1和instance2同时赋予属性值时
        //会看到产生了不同的hashCode
    }

}

  • 解决反射破坏方法

    private static boolean instanceCreated = false;

    private Hungry(){
            //防止单例被破坏
            if (instanceCreated) {
                throw new RuntimeException("请使用 Hungry.getInstance() 方法获取一个单例实例");
            }
            instanceCreated = true;
        System.out.println(Thread.currentThread().getName() + " is creating an instance.");
    }
    
  1. 序列化破坏单例

如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,
可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象

  • 序列化破坏单例
public class SerializationTest {

    @Test
    public void serializaStudentTest() throws Exception {
        Student student1 = Student.getInstance();

        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.ser"));
        oos.writeObject(student1);
        oos.close();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.ser"));
        Student student2 = (Student) ois.readObject();
        ois.close();

        System.out.println(student1 == student2);  // 输出: true
    }


    @Test
    public void serializaHungryTest() throws Exception {
        Hungry hungry1 = Hungry.getInstance();

        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hungry.ser"));
        oos.writeObject(hungry1);
        oos.close();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hungry.ser"));
        Hungry hungry2 = (Hungry) ois.readObject();
        ois.close();
        //当Hungry单例中没有重写readResolve方法时,student1 == student2输出的对象地址为fasle,表明生成了不同的实例对象
        System.out.println(hungry1 == hungry2);  // 输出: true
    }
}

  • 解决序列化破坏单例
    public Object readResolve() throws ObjectStreamException {
        return hungry;
    }

单例测试调用示例

public class SingletonTest {

    /**
     * @Author: zhaozuofa
     * @Description: 饿汉式调用测试
     * @Date: 2024/7/9 11:25
     * @Param:
     * @return: void
    **/
    @Test
    public void useHungryTest() {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                //通过无参构造中输出的线程名判断,在单例模式下内存中应该只有全局唯一的对象实例,即当第一个线程会调用无参构造,完成类的实例化,
                //其余线程将直接获得由单例创模式创建的实例对象,即只打印一个线程名
                Hungry.getInstance();
            }).start();
        }
    }

    /**
     * @Author: zhaozuofa
     * @Description: 懒汉式调用测试
     * @Date: 2024/7/9 13:52
     * @Param:
     * @return: void
    **/
    @Test
    public void useLayManTest() {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                //通过无参构造中输出的线程名判断,在单例模式下内存中应该只有全局唯一的对象实例,即当第一个线程会调用无参构造,完成类的实例化,
                //其余线程将直接获得由单例创模式创建的实例对象,即只打印一个线程名
                LazyMan.getInstance();
            }).start();
        }
    }


    /**
     * @Author: zhaozuofa
     * @Description: 饿汉式-双重检验锁(double check lock)(DCL)
     * @Date: 2024/7/9 13:52
     * @Param:
     * @return: void
     **/
    @Test
    public void useLazyDoubleCheckManTest() {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                //通过无参构造中输出的线程名判断,在单例模式下内存中应该只有全局唯一的对象实例,即当第一个线程会调用无参构造,完成类的实例化,
                //其余线程将直接获得由单例创模式创建的实例对象,即只打印一个线程名
                LazyDoubleCheckMan.getInstance();
            }).start();
        }
    }

    /**
     * @Author: zhaozuofa
     * @Description: 饿汉式-双重检验锁(double check lock)(DCL)-完善版
     * @Date: 2024/7/9 13:52
     * @Param:
     * @return: void
     **/
    @Test
    public void useLazyDoubleCheckImproveManTest() {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                //通过无参构造中输出的线程名判断,在单例模式下内存中应该只有全局唯一的对象实例,即当第一个线程会调用无参构造,完成类的实例化,
                //其余线程将直接获得由单例创模式创建的实例对象,即只打印一个线程名
                LazyDoubleCheckImproveMan.getInstance();
            }).start();
        }
    }

    /**
     * @Author: zhaozuofa
     * @Description: 静态内部内 单例模式 (推荐使用)
     * @Date: 2024/7/9 13:52
     * @Param:
     * @return: void
     **/
    @Test
    public void useStaticInnerTest() {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                //通过无参构造中输出的线程名判断,在单例模式下内存中应该只有全局唯一的对象实例,即当第一个线程会调用无参构造,完成类的实例化,
                //其余线程将直接获得由单例创模式创建的实例对象,即只打印一个线程名
                StaticInner.getInstance();
            }).start();
        }
    }

    /**
     * @Author: zhaozuofa
     * @Description: 静态内部内 单例模式 (推荐使用)
     * @Date: 2024/7/9 13:52
     * @Param:
     * @return: void
     **/
    @Test
    public void useStudentTest() {
//        for (int i = 0; i < 10; i++) {
//            new Thread(() -> {
        //通过无参构造中输出的线程名判断,在单例模式下内存中应该只有全局唯一的对象实例,即当第一个线程会调用无参构造,完成类的实例化,
        //其余线程将直接获得由单例创模式创建的实例对象,即只打印一个线程名
//                Student.getInstance();
//            }).start();
//        }
        // 获取单例实例
        Student student1 = Student.getInstance();
        Student student2 = Student.getInstance();

        // 操作Student对象
        System.out.println(student1 == student2); // 输出: true,证明是同一个实例
    }

}
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明原文出处。如若内容造成侵权/违法违规/事实不符,请联系SD编程学习网:675289112@qq.com进行投诉反馈,一经查实,立即删除!