单例模式的各种实现方式(Java)

单例模式的基础实现方式

手写普通的单例模式要点有三个:

  • 将构造函数私有化
  • 利用静态变量来保存全局唯一的单例对象
  • 使用静态方法 getInstance() 获取单例对象

懒汉模式

懒汉模式指的是单例对象的延迟加载,只有在调用 getInstance() 获取单例对象时才会将单例创建出来。懒汉模式适用于对内存要求高的场景。代码如下:

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

饿汉模式

与懒汉模式相对的是饿汉模式,适用于对内存要求不高的场景,在类加载的初始化阶段就完成了单例对象的创建,代码如下:

public class Singleton {
    // 静态变量初始化
    private static Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

静态变量的初始化是在类加载阶段的初始化过程进行,在此期间,编译器会自动收集类中所有静态变量的赋值动作和 static 块,生成 <clinit> 方法并执行。比较特殊的一点是,如果多个线程同时初始化 Singleton 类,JVM 会保证只有一个线程能够执行 Singleton 类的 <clinit> 方法,其他线程都必须阻塞等待。而且同一个类加载器下,一个类只会被初始化一次,即 <clinit> 方法只会被执行一次,这就保证了多线程下单例对象只会被创建一次

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15847802.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

多线程下的单例模式

单例模式需要保证的一点是,在整个程序运行期间,单例对象只会被创建一次。如果是单线程环境中,这一点很好保证。但如果是多线程环境中,保证这一点并不简单

上面已经说过,饿汉模式的单例模式下,JVM 会保证单例对象只会被创建一次,因此可以保证这一点。而懒汉模式在多线程环境中不能保证这一点,接下来讨论的是对懒汉模式进行改造,让它能够保证这一点

使用synchronized方法

最简单直接的方式就是为 getInstance() 加上 synchronized 关键字,这样确实可以保证多线程环境中,单例对象只会被创建一次。但是 synchronized 方法最大的缺点在于它将获取单例对象这一行为彻底串行化,同一时刻只能有一个线程能执行 getInstance() ,大大降低了并发效率
代码如下:

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

双重检测锁

直接使用 synchronized 方法降低效率的主要原因在于,synchronized 方法的加锁粒度太粗,那么将锁的范围缩小,就可以缓解这一问题,而双重检测锁就是这么实现的。不过为了保证并发的正确性,在内部又加了一道检测,故名为双重检测锁。代码如下:

public class Singleton {
    // 这里的instance一定要定义为volatile变量!!!
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        // 双重锁检测
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

上面代码的关键点有三个:

  • synchronized 加锁的范围更小,这是为了更高的并发效率
  • synchronized 内部还有一道检测,如果线程1进入了同步块,但还未将单例对象创建出来,此时线程2正好绕过了第一道检测,在同步块外等待获取锁定。因此同步块内也要加上一道检测,避免单例对象被重复创建
  • instance 这个变量一定要声明为 volatilevolatile 在这里最大的作用是禁止指令重排序。如果不加 volatile 修饰,由于 instance = new Singleton() 可能被重排序而导致在这条语句执行过程中,instance 率先被分配内存并获得地址,成为非 null,但构造函数却没有真正执行完毕,此时别的线程可能拿到的 instance 就是不完全构造的单例对象

instance = new Singleton() 这条语句正常的执行顺序是:
1、为即将创建的对象分配一块内存
2、执行构造函数中的语句,对内存进行相应的读写操作
3、让 instance 指向这块内存
在重排序情况下顺序可能是 1 -> 3 -> 2,当执行到3时 instance 就成为非 null,此时其他线程如果引用了 instance,拿到的就是一个不完全构造的对象

需要注意的是,在 JDK5 之前,就算加了 volatile 关键字也依然有问题,原因是之前的 JMM 是有缺陷,volatile 变量前后的代码仍然可以出现重排序问题,这个问题在 JDK5 之后才得到解决,所以现在才可以这么使用

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15847802.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

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

反射

枚举方式外, 其他方法都会通过反射的方式破坏单例。反射其实就是越过 private 限制,直接调用单例类的构造方法,生成新的对象

解决方式:在构造方法中进行判断,若已存在实例,则阻止生成新的实例。代码如下:

// 单例类的private构造方法
private Singleton() {
    if (instance != null){
        throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
    }
}

序列化和反序列化

如果单例类实现了 Serializable 接口,就可以通过反序列化方式破坏单例模式

解决方式:
1、不实现 Serializable 接口
2、如果必须要实现 Serializable 接口,可以重写反序列化方法 readResolve(),调用该方法会直接返回已经创建好的单例对象。代码如下:

public Object readResolve() throws ObjectStreamException {
    return instance;
}
作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15847802.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

其他单例模式的实现方式

基于枚举类

基于枚举类的方式非常简洁,只要简单地编写一个只包含一个元素的枚举类,由 JVM 来保证单例的唯一性和线程安全性,自带私有的构造方法并且序列化和反射都不会破坏单例的唯一性,据说是 JDK5 之后最好的单例创建方式

public enum Singleton {
    instance;
    
    // 定义各种字段、方法
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return instance;
    }
}

其中枚举类的构造器不用特意加上 private 修饰,因为枚举类构造器默认就是 private 的,且只能使用 private 修饰

简单理解枚举实现单例的过程:程序启动时,会自动调用 Singleton 的构造器,实例化单例对象并赋给 instance,之后再也不会实例化,这也是一个饿汉过程,即使没有调用过 getInstance(),也会将单例对象创建出来

使用枚举来创建单例模式的优势有3点:

  • 代码量更少,更加简洁
  • 没有做任何额外的操作,就可以保证单例的唯一性和线程安全性
  • 使用枚举类可以防止调用者使用反射、序列化和反序列化机制强制生成多个单例对象,破坏唯一性

这第三点优势让基于枚举类的单例模式变得“无懈可击”了,枚举类可以保证唯一性的原理如下:

  • 防反射

枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是枚举类,如果是则抛出异常

  • 防反序列化创建多个枚举对象

对于枚举类型,由于枚举类和枚举变量的组合名是唯一的,可以唯一确定对象。因此,序列化只会将枚举类名 + 枚举变量名输出到文件中。反序列化时,读入的就是枚举类名 + 枚举变量名,再根据 Enum 类的 valueOf 方法,在内存中找对已经存在的枚举对象,并不会创建新的对象

类加载器对单例模式的影响

同一个类加载器对一个类只会加载一次,但是不同的类加载器可能会多次加载同一个类,如果程序中有多个类加载器,需要在单例中指定某个特定的类加载器,并保证这个类加载器始终是同一个

posted @ 2022-01-26 18:41  酒冽  阅读(470)  评论(1编辑  收藏  举报