【设计模式】单例模式

本文详细介绍了Java中的单例模式,包括饿汉式和懒汉式的实现,以及如何通过静态内部类和枚举来实现线程安全的单例。还讨论了单例模式可能被序列化和反射破坏的情况,并提供了防范措施。最后总结了单例模式的优缺点及其适用场景。
摘要由CSDN通过智能技术生成

慢慢来,才比较快

单例模式属于对象创建型模式,它要求系统中只生成一个实例,提供了一种创建对象的最佳方式

?那么如何保证类中只有一个实例并且这个实例易于被访问呢

一个很好的解决的方法就是让这个类本身保存它的唯一实例,这个类需要保证没有其他实例被创建,并且它可以提供访问该实例的方法。

所以我们可以得出单例模式的定义:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类,它提供一个public方法。

因此单例模式的一个特点就是构造方法是私有的,这样就可以防止外界随意调用来产生实例(但是反射可以改变权限)

单例模式分为懒汉式和饿汉式,饿汉式是类加载的时候就初始化,而懒汉式是使用的时候才加载

单例模式的实现

饿汉式:

  • 采用静态成员变量,在类加载的时候创建实例,如果用不到这个类会造成内存资源的浪费,因为单例实例引用不可变,所以是线程安全的
public class Singleton {

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

    //2.在本类中创建该类对象
    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return instance;
    }
}
  • 第一种方法的变种
public class Singleton {

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

    //声明Singleton类型的变量
    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    public static Singleton getInstance(){
        return instance;
    }
}
  • 通过枚举类实现单例模式,我们知道枚举反编译之后实际是由jvm创建一个final类去继承Enum类,通过static来加载,因此也属于饿汉式,枚举是线程安全的并且只会被装载一次,是唯一一种不会被破坏的单例模式。Java反编译以及语法糖
public enum EnumInstance {

    INSTANCE;
   
}

懒汉式:

public class Singleton {
    private Singleton() {}

    private static Singleton instance;

    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}
  • 但是这种写法,很明显不是线程安全的。如果多个线程在该类初始化之前,有大于一个线程调用了getinstance方法且lazySingleton == null 判断条件都是正确的时候,这个时候就会导致new出多个LazySingleton实例。
  • 解决的话可以在getInstance方法上加sychronized锁,但这种方式锁粒度太大,学过并发编程的同学应该都会记得有一个DCL(DoubleCheckedLocking),所以这里改进可以使用双重检查的方式
public class Singleton {

    private Singleton(){}

    private static Singleton instance;

    public static Singleton getInstance(){
    //先判断是否已经创建了实例
        if(instance == null){
        //当有多个线程进入这里时,加锁控制,只有一个线程可以进入下一步,如果条件成立则new出来
        //一个实例,轮到其他的线程判断的时候自然就就为假了,问题大致解决。
            synchronized (Singleton.class){
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

对JMM熟悉的同学应该会发现,就算用了双重检查,也还是会出现问题。因为在编译过程中,为了提高性能,编译器和处理器常常会对指令做重排序,重排序对单线程并没有影响,但是在多线程情况下就有可能得出意料之外的结果。

eg:

instance = new Singleton();

执行的时候正常是这样的:

  • 分配内存给这个对象
  • 初始化对象
  • 设置Singleton指向刚分配的内存地址。

但重排序之后就有可能变为这样:

  • 分配内存给这个对象
  • 设置Singleton指向刚分配的内存地址
  • 初始化对象

这样就还是会造成多线程的问题,比如说有两个线程 t1 和 t2 ,t1进入了第一个if之中,并且拿到了锁,进行了new Singleton();语句,在加载构造类的实例的时候,设置Singleton指向刚分配的内存地址,但是还没有初始化对象。线程2判断 if(instance == null) 为假,直接返回了 instance ,但此时还没有初始化,这就出了问题。

当然这个很好改进,从禁用重排序方面下手,添加一个volatile

    private volatile static Singleton instance = null;

还有一个方法可以很好的避免这个问题:
定义一个静态内部类(基于类初始化的延迟加载),其静态字段实例化了一个单例。获取单例需要调用getInstance方法间接获取。

public class Singleton {
    private Singleton() {}

    private static class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

单例模式的破坏和防范

序列化破坏

对饿汉式和懒汉式都起效

public class Singleton implements Serializable {

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

    //2.在本类中创建该类对象
    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return instance;
    }
}
  • 单例类先实现序列化接口
/**
 * @Author: ekin
 * @Date: 2021/7/3 17:45
 */

public class Client {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singleton instance = Singleton.getInstance();

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
        oos.writeObject(instance);

        File file = new File("singleton");

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        Singleton newInstance = (Singleton) ois.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • 测试
    在这里插入图片描述

结果发现对象不一样,原因就涉及到序列化的底层原因了,我们先看解决方式:

单例类代码中添加下面这段代码

    private Object readResolve() {
        return instance;
    }

结果:
在这里插入图片描述原因:

  • 在反序列化时,程序会检查如果单例类有readResolve方法时,则调用该方法的返回值,如果没有,则通过反射创建一个新实例
    在这里插入图片描述

反射破坏

public class Client {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class clazz = Singleton.class;

        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);//打开构造器权限
        Singleton instance = Singleton.getInstance();

        Singleton newInstance = (Singleton) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

在这里插入图片描述

这里强行破开了private的构造方法的权限,使得能new出来一个单例实例。

解决:

饿汉式:

    private Singleton() {
        if (instance != null){
            throw new RuntimeException("禁止反射调用");
        }
    }

在这里插入图片描述懒汉式:

//当通过构造器产生实例时,该类没有实例
Singleton instance = constructor.newInstance();
Singleton newInstance = Singleton.getInstance();
  • 暂时无法防止

枚举单例模式的好处

防范序列化破坏单例:

  • 当进行反序列化读取时,调用顺序为
    在这里插入图片描述
  • 最后一个方法读取的仍然是原来的实例

防范反射破坏单例:

  • 当进行反射的实例获取时,会出现异常
/**
 * @Author: ekin
 * @Date: 2021/7/3 20:52
 */
public class Client {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class clazz = com.ekin.softwaredesign.singleton.demo6.Singleton.class;

        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        com.ekin.softwaredesign.singleton.demo1.Singleton instance = com.ekin.softwaredesign.singleton.demo1.Singleton.getInstance();

        com.ekin.softwaredesign.singleton.demo1.Singleton newInstance = (Singleton) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);

    }
}
  • 原因是在 Constructor 中有这么一段话
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
  • 表示如果是枚举则不能反射。

单例模式总结

优点:

  • 在内存中只有一个实例,减少了资源的开销
  • 可以避免对资源的多重占用

缺点:

  • 没有接口,扩展困难

单例模式可以跟工厂模式搭配使用。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值