欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 幼教 > 设计模式-单例模式的完整代码示例及测试验证

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

2025/2/23 0:56:57 来源:https://blog.csdn.net/u011752272/article/details/140323497  浏览:    关键词:设计模式-单例模式的完整代码示例及测试验证

单例模式

什么是单例模式?

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

优缺点

  • 优点
  1. 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  2. 可以避免对资源的多重占用。
  3. 单例模式设置全局访问点,可以优化和共享资源的访问。
  • 缺点
  1. 单例模式一般没有接口,扩展困难。
  2. 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则

单例模式实现方式

通常有如下5种写法:

  1. 懒汉式 ——>LazyMan
/*** @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;}}
  1. 饿汉式 ——>Hungry
@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;
}
  1. 静态内部类(推荐) ——>StaticInner
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;}
}
  1. 双重校验锁 ——>LazyDoubleCheckMan,LazyDoubleCheckImproveMan
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;}
}
  1. 枚举(推荐) ——>Student
@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 {@Testpublic 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();}}@Testpublic 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); // 打印结果为 falseSystem.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 {@Testpublic 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}@Testpublic 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**/@Testpublic 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**/@Testpublic 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**/@Testpublic 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**/@Testpublic 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**/@Testpublic 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**/@Testpublic 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,证明是同一个实例}}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词