【深入理解设计模式】单例设计模式

本文详细介绍了单例模式的概念、分类(饿汉式、懒汉式、枚举实现、双重检查锁等),以及如何通过不同方式实现、保证线程安全,并探讨了破坏单例模式的方法及解决策略。
摘要由CSDN通过智能技术生成

在这里插入图片描述

单例设计模式

在这里插入图片描述

概念:

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。

单例设计模式是一种创建型设计模式,其主要目的是确保类在应用程序中的一个实例只有一个。这意味着无论在应用程序的哪个位置请求该类的实例,都将获得同一个实例。这种模式通常用于控制某些共享资源的访问,或者在整个应用程序中管理唯一的状态。

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

单例模式的实现:

单例设计模式分类两种:

  1. 饿汉式:类加载就会导致该单实例对象被创建
  2. 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
饿汉式实现方式:
饿汉式实现1 - 静态变量方式
/**
* @author OldGj 
* @version v1.0
* @apiNote 单例设计模式 - 饿汉式:静态变量实现
*/
public class Singleton {

   // 1.构造方法私有化
   private Singleton() {}

   // 2.在成员位置创建本类的对象
   private static final Singleton instance = new Singleton();

   // 3.对外界提供可以访问到自身创建的对象的静态方法
   public static Singleton getInstance(){
       return instance;
   }
}

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

饿汉式实现2 - 静态代码块方式

拓展:静态代码块在类加载过程的初始化阶段执行。
类的加载过程分为: 加载 -> 链接验证准备解析) -> 初始化

/**
 * @author OldGj 
 * @version v1.0
 * @apiNote 单例设计模式 - 饿汉式:静态代码块实现
 */
public class Singleton {

    // 1.构造器私有化
    private Singleton(){}

    // 2.声明静态全局变量
    private static Singleton instance;

    // 3.在静态代码块中进行赋值
    static {
        instance = new Singleton();
    }

    // 4.对外提供可以获取本类对象的静态方法
    public static Singleton getInstance(){
        return instance;
    }
}

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

饿汉式实现3 - 枚举

枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

/**
* @author OldGj 
* @version v1.0
* @apiNote 单例模式 - 饿汉式 > 利用枚举实现 <
*/
public enum Singleton {
   INSTANCE;
}

注意:
枚举方式属于饿汉式方式。如果在不考虑饿汉式存在内存浪费问题的情况下使用枚举类型创建单例模式是最好的单例实现方式,因为枚举类型是天然的线程安全的,并且实现方式简单,只会被加载一起,而且是所有单例实现方式中,唯一一种不会被破坏的实现方式。

懒汉式实现方式:
懒汉式实现1 - 会有线程安全问题
/**
* @author OldGj
* @version v1.0
* @apiNote 单例设计模式 - 懒汉式 > 线程不安全 <
*/
public class Singleton {

   // 1.构造器私有化
   private Singleton() {
   }

   // 2.在成员位置创建本类的对象
   private static Singleton instance;

   // 3.对外提供可以获取到本类对象的静态方法
   public static Singleton getInstance() {
       // 只有在instance变量还没有实例化对象的时候,才实例化一个对象
       if (instance == null) {
           // 线程1 -> 等待
           // 线程2,分配到CPU,实例化了一个Singleton实例并赋值给instance变量
           // 线程1,因为已经进入判断,因此线程1又实例化了一个Singleton对象,不符合单例模式
           instance = new Singleton();
       }
       return instance;
   }
}

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

懒汉式实现2 - synchronized同步锁解决线程安全问题
/**
 * @author OldGj 
 * @version v1.0
 * @apiNote 单例设计模式 - 懒汉式  > 线程安全 <
 */
public class Singleton {

    // 1.构造器私有化
    private Singleton (){}

    // 2.在成员位置声明Singleton类型的静态变量
    private static Singleton instance;

    // 3.对外提供一个可以获取本类对象的静态方法
    // 并且将该方法使用synchronized同步锁加锁,确保实例化过程中的线程安全
    public static synchronized Singleton getInstance(){
        if(instance==null){
            instance = new Singleton();
        }
        return instance;
    }
}

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

懒汉式实现3 - 双重检查锁

再来讨论一下懒汉模式中加锁的问题,对于 getInstance() 方法来说,绝大部分的操作都是读操作读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式


/**
 * @author OldGj 
 * @version v1.0
 * @apiNote 单例设计模式 - 懒汉式 <b> > 双重检查锁模式 < <b/><br/>
 *          <p>多线程环境下,不会出现线程安全问题,也不会有性能问题*推荐使用*<p/>
 */
public class Singleton {

    // 1.构造器私有化
    private Singleton(){}

    // 2.在成员位置声明Singleton类型的静态变量并用volatile关键字修饰
    private static volatile Singleton instance;

    // 3.对外提供可以获取本类对象的静态方法,并在方法内采用双重检查锁保证线程安全
    public static Singleton getInstance(){
        // 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance==null){
            synchronized (Singleton.class){
                // 抢到锁之后再次判断是否为null
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}

注意:
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。

添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

懒汉式实现4 - 静态内部类方式

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

/**
 * @author OldGj 
 * @version v1.0
 * @apiNote 单例设计模式 - 懒汉式 > 静态内部类实现 <
 */
public class Singleton {
    /**
     * 静态内部类单例模式中实例由内部类创建,
     * 由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载,
     * 并初始化其静态属性。静态属性由于被 `static` 修饰,保证只被实例化一次,并且严格保证实例化顺序。
     */


    // 1.构造器私有化
    private Singleton() {}

    // 2.创建静态内部类,并且在静态内部类中创建外部类的实例
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    // 3.对外提供可以获得本类实例的静态方法
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

说明:

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

小结:

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

破坏单例模式

单例模式主要是通过把一个类的构造器私有化,来避免重复创建多个对象的。如果想要破坏单例模式,就要想办法执行到私有的构造方法即可!

这里有两种方式可以破坏单例模式,分别是:

1.使用反射机制
2. 序列化 / 反序列化

序列化 / 反序列化

我们先通过静态内部类的方式定义一个单例模式的类Singleton:

/**
 * @author OldGj 
 * @version v1.0
 * @apiNote 破坏单例模式 - 序列化/反序列化
 */
public class Singleton implements Serializable {

    // 1.构造器私有化
    private Singleton() {
    }

    // 2.创建静态内部类,并且在静态内部类中创建外部类的实例
    private static class SingletonHandel {
        private static final Singleton INSTANCE = new Singleton();
    }

    // 3.对外提供可以获取到本类对象的静态方法
    public static Singleton getInstance() {
        return SingletonHandel.INSTANCE;
    }
    
}

然后通过序列化 / 反序列化的方式,尝试创建该单例模式的实例:

/**
 * @author OldGj 
 * @version v1.0
 * @apiNote 测试类 - 测试序列化/反序列化会不会破坏单例模式
 */
public class Client {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
//        writeObjToFile();
        readObjFromFile();
        readObjFromFile();
    }

    public static void readObjFromFile() throws IOException, ClassNotFoundException {
        // 1.创建对象输入流对象
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("C:\\Users\\HX\\Desktop\\a.txt"));
        // 2.读取对象
        Singleton singleton = (Singleton) objectInputStream.readObject();
        System.out.println(singleton); // 输出的地址不一样,说明:序列化/反序列化会破坏单例模式
        // 3.关闭资源
        objectInputStream.close();
    }

    /**
     * 序列化对象,写道磁盘文件中
     * @throws IOException io异常
     */
    public static void writeObjToFile() throws IOException {
        // 1.创建对象输出流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\HX\\Desktop\\a.txt"));
        // 2.创建对象
        Singleton instance = Singleton.getInstance();
        // 3.对外写出对象
        oos.writeObject(instance);
        // 4.关闭资源
        oos.close();
    }
}

通过以上代码测试,我们发现控制台输出的Singleton对象的地址是不同的,也就是说通过序列化 / 反序列化方式破坏了单例模式。

反射机制

首先,我们先使用双重判断锁的方式实现一个Singleton单例模式:

/**
 * @author OldGj 
 * @version v1.0
 * @apiNote 破坏单例模式 - 反射
 */
public class Singleton {

    //1.构造器私有化
    private Singleton() {}

    //2.在成员位置声明本类的静态变量
    private static volatile Singleton instance;

    //3.对外提供可获得本类实例的静态方法
    public static Singleton getInstance() {

        /*
         * 双重检查锁
         */
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

然后我们编写测试类,测试通过反射机制是否可以破坏单例模式:

/**
 * @author OldGj 
 * @version v1.0
 * @apiNote 测试类 - 测试 反射机制是否会破坏单例模式
 */
public class Client {
    public static void main(String[] args) throws Exception {

        // 1.获取单例对象的字节码对象
        Class clazz = Singleton.class;
        // 2.获取无参构造
        Constructor constructor = clazz.getDeclaredConstructor();
        // 3.关闭访问权限
        constructor.setAccessible(true);
        // 4.创建对象
        Singleton s1 = (Singleton) constructor.newInstance();
        Singleton s2 = (Singleton) constructor.newInstance();
        // 如果返回true,说明反射不会破坏单例模式,如果返回false,说明反射会破坏单例模式
        System.out.println(s1 == s2);

    }
}

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

注意:通过枚举方式实现的饿汉式单例模式不会被以上两种方式破坏单例模式。

解决破坏单例模式

破坏单例模式的方式有两种,分别是序列化 / 反序列化以及反射机制,我们先来通过代码实现如何防止序列化 / 反序列化方式破坏单例模式:

序列化、反序列方式破坏单例模式的解决方法
/**
 * @author OldGj 
 * @version v1.0
 * @apiNote 破坏单例模式 - 序列化/反序列化
 */
public class Singleton implements Serializable {

    // 1.构造器私有化
    private Singleton() {
    }

    // 2.创建静态内部类,并且在静态内部类中创建外部类的实例
    private static class SingletonHandel {
        private static final Singleton INSTANCE = new Singleton();
    }

    // 3.对外提供可以获取到本类对象的静态方法
    public static Singleton getInstance() {
        return SingletonHandel.INSTANCE;
    }

    // 4.定义readResolve方法,解决由于序列化/反序列化造成的破坏单例模式
    /*
     * 如果定义了readResolve方法,在对象进行反序列化时,就会调用该方法返回对象
     * 如果没有定义,才会new新的对象
     * @return
     */
    public Object readResolve() {
        return SingletonHandel.INSTANCE;
    }

}

想要解决因为序列化 / 反序列化方式破坏单例模式的问题,我们只需要在Singleton单例模式类中自定义一个readResolve方法,在该方法中返回我们自己创建的单例对象即可,因为在反序列化的底层,ObjectInputSteam对象会判断反序列化对象是否定义了readResolve方法,如果定义,则会执行该方法,如果没有定义才会new新的对象。
源码解析:
ObjectInputStream类

public final Object readObject() throws IOException, ClassNotFoundException{
    ...
    // if nested read, passHandle contains handle of enclosing object
    int outerHandle = passHandle;
    try {
        Object obj = readObject0(false);//重点查看readObject0方法
    .....
}
    
private Object readObject0(boolean unshared) throws IOException {
	...
    try {
		switch (tc) {
			...
			case TC_OBJECT:
				return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
			...
        }
    } finally {
        depth--;
        bin.setBlockDataMode(oldMode);
    }    
}
    
private Object readOrdinaryObject(boolean unshared) throws IOException {
	...
	//isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
    obj = desc.isInstantiable() ? desc.newInstance() : null; 
    ...
    // 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
    if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
    	// 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
    	// 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
    	Object rep = desc.invokeReadResolve(obj);
     	...
    }
    return obj;
}
反射方式破解单例的解决方法
/**
 * @author OldGj 
 * @version v1.0
 * @apiNote 破坏单例模式 - 反射
 */
public class Singleton {

    // 是否多次调用构造器
    private static boolean flag = false;

    //1.构造器私有化
    private Singleton() {
        /*
            解决通过反射破坏单例模式
         */
        synchronized (Singleton.class) {
            if (flag) {
                throw new RuntimeException("不能创建多个对象");
            }
            //在第一次调用构造器之后,flag设为true,下次调用则抛出异常
            flag = true;
        }
    }

    //2.在成员位置声明本类的静态变量
    private static volatile Singleton instance;

    //3.对外提供可获得本类实例的静态方法
    public static Singleton getInstance() {

        /*
         * 双重检查锁
         */
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这种方式比较好理解。当通过反射方式多次调用构造方法进行创建创建时,直接抛异常。不运行此中操作。

  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值