设计模式 - 单例模式

1. 什么是单例模式

单例模式是指在内存中只会创建也且仅创建一次的对象的设计模式, 在程序中多次使用同一个多想且作用相同时,为了防止频繁地创建和销毁对象使得内存飙升,单例模式可以使程序仅在内存中创建一个对象,让所有调用该对象的地方都共享这一单例对象。

优点:

  • 在内存中只有一个实例,减少了内存的开销,尤其是频繁地创建和销毁实例,同时也减少了GC
  • 避免对资源的多重占用,例如对文件的操作
  • 通过这个全局访问点为抓手,可以对资源的访问做优化处理,例如访问点中预置缓存

缺点:

  • 没有接口,不能继承,使扩展困难,违背了开闭原则
  • 单例模式的功能一般写于一个类中,这会导致业务逻辑耦合,与单一责任原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么实例化
  • 单例模式只存在一个实例,在并发测试中不利于调试

2. 单例模式的实现方式

1. 懒汉式

真正需要使用对象时才去创建该单例对象,懒汉式创建对象的方法是在程序使用对象前,先判断对象是否已经实例化(判空),若已实例化则直接返回该类对象,否则执行实例化操作。

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这种方式是单例模式的最简单的实现方式,但这种实现方式在并发情况下并不安全,如果两个线程同时判断对象实例为null, 那么两个线程就会分别实例化对象,从严格意义上来说它并不属于单例模式。

所以我们要解决的是线程安全问题,最简单的方式加锁–synchronized。

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这样就规避了两个线程同时去实例化对象的风险,但是这会引来另一个问题:每次获取对象都需要先获取锁,并发性能非常差,效率非常低,因为99%情况下是不需要同步的

2. 饿汉式

饿汉式在类加载时就已经创建好该对象,在调用获取对象实例时直接返回该单例对象即可,不需要等到被调用时才去创建。

public class Singleton {

    private static final Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    } 

}

这种方式基于classloader机制避免了多线程的同步问题,不会产生多个Singleton对象,也不需要加锁,执行效率会提高,但是在类加载时就初始化,会浪费内存。

3. 双重校验锁(DCL, double-checked locking)

双重校验锁机制其实是懒汉式的一种升级,不仅保证了线程安全,并且在多线程情况下能保持高性能。

public class Singleton {

    private volatile static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singlenton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}

上面的代码可以完美解决并发安全和性能低效的问题,

  • 如果instance不为空,则直接返回对象,不需要获取锁,如果多个线程发现instance为空,则进入if分支
  • if分支内,多个线程会去抢同一个Class锁, 只有一个线程会抢成功,获取到class锁的线程会再次判断instance是否为空,因为instance有可能被之前线程实例化了
  • 之后获取到class锁的线程,会发现instance已经不为空了,则不会再去实例化对象,直接返回
  • 之后所有的进入该方法的线程都不会获取锁,因为instance已经被实例化了,不会为null

volatile 防止指令重排序

首先了解下指令重排序是什么。

指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序性能

在JVM创建对象时,会经历以下步骤:

  1. 判断对象对应的类是否加载、链接、初始化
  2. 为对象分配内存
  3. 处理并发问题
  4. 初始化分配到的内存
  5. 设置对象的对象头
  6. 执行init方法进行初始化

在第6步,需要初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。这有可能会发生指令重排序现象,比如先把堆内对象的首地址赋值给引用变量,然后再初始化对象,有可能在线程A先把堆内对象的首地址赋值给引用变量,此时线程B判断instance并不为空,但实际上线程A并还没完全初始化Singleton对象,线程B就会报NPE异常。

4. 静态内部类

这种方式可以实现跟双重校验锁方式一样的效果,和饿汉式同样利用了classloader机制来保证线程安全,但是又有点不一样。饿汉式只要Singleton类被装载,那么instance就会被实例化,而这种方式,即使Singleton类被装载了,但是instance不一定被实例化,因为SingletonHolder类没有被主动使用,只有显式地调用getInstance方法时才会显示地装载SingletonHolder,从而实例化instance。如果instance很耗资源想让他延迟加载,另一方面又不想Singleton类加载时就实例化,因为Singleton可能在其他地方会显式调用,那么这时候这种方式比饿汉式是更加合理的。

public class Singleton {

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

    private Singleton() {}

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

5. 枚举

public enum Singleton {

    INSTANCE;

    Singleton(){}

}

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化,也不能通过 reflection attack 来调用私有构造方法。

枚举是如何实现单例的呢?

在程序启动时,会调用枚举类的空参构造器,实例好一个Singleton对象赋给INSTANCE,之后再也不会实例化

枚举是如何防止反射的呢?

@CallerSensitive
public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
           IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。
另外枚举类的无参构造器并不是真的无参,编译后的枚举类构造器的参数是(String.class, int.class)

枚举类是如何防止反序列化的呢?

在读入Singleton对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。

所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。

破坏单例模式

反射破坏单例模式

饿汉式和懒汉式

public static void main(String[] args) {
    Singleton instance1 = Singleton.getInstance();
    Singleton instance2 = Singleton.getInstance();

    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton instance3 = constructor.newInstance();
    System.out.println(instance1 == instance2); // true
    System.out.println(instance1 == instance3); // false
}

枚举

public static void main(String[] args) {
    Singleton instance1 = Singleton.INSTANCE;
    Singleton instance2 = Singleton.INSTANCE;

    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton instance3 = constructor.newInstance();
    System.out.println(instance1 == instance2);
    System.out.println(instance1 == instance3);

}

Exception in thread “main” java.lang.NoSuchMethodException: com.example.demo.design.pattern.singleton.enums.Singleton.()

at java.lang.Class.getConstructor0(Class.java:3082)

at java.lang.Class.getDeclaredConstructor(Class.java:2178)

at com.example.demo.design.pattern.singleton.enums.TestEnums.main(TestEnums.java:25)

错误显示没有无参构造器,经过反编译来看,枚举类构造器确实是有参的

public static void main(String[] args) {
    Singleton instance1 = Singleton.INSTANCE;
    Singleton instance2 = Singleton.INSTANCE;

    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
    constructor.setAccessible(true);
    Singleton instance3 = constructor.newInstance();
    System.out.println(instance1 == instance2);
    System.out.println(instance1 == instance3);

}

报错不允许反射创建枚举类, 源码:Constructor.newInstance()

Exception in thread “main” java.lang.IllegalArgumentException: Cannot reflectively create enum objects

at java.lang.reflect.Constructor.newInstance(Constructor.java:417)

at com.example.demo.design.pattern.singleton.enums.TestEnums.main(TestEnums.java:27)

反序列化破坏单例模式

饿汉式和懒汉式

public static void main(String[] args){ 
    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
    outputStream.writeObject(Singleton.getInstance());
    File file = new File("Singleton.file");
    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
    Singleton instance = (Singleton) inputStream.readObject();
    System.out.println(instance == Singleton.getInstance()); // false

}

枚举

public static void main(String[] args) {

    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
    outputStream.writeObject(Singleton.INSTANCE);
    File file = new File("Singleton.file");
    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
    Singleton instance = (Singleton) inputStream.readObject();
    System.out.println(instance == Singleton.INSTANCE); // true
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值