单例模式的几种实现!!!

发布于 2021-04-22  755 次阅读


单例模式是非常重要的设计模式,在框架中使用单例模式可以保证某些工具类不会被多次创建而造成资源浪费,特此记录单例模式的几种实现

 

什么是单例模式呢?

类中只有一个实例,且该类能自行创建这个实例

其三大特点为:

1.只有一个实例对象

2.单例对象必须由单例类自行创建

3.单例类对外提供一个访问该单例的全局访问点

实现一:懒汉单例

懒汉单例,即在真正需要的时候才去创建实例;在getInstance(对外提供的访问点)方法中,先判断实例是否为空再决定是否去创建实例,看上去很完美。

首先我们创建需要实现单例模式的类User,过程如下

看上去我们确实创建了一个单例对象 User ,但是,存在线程安全问题,在并发获取实例的时候,可能会存在构建了多个实例的情况,所以需要改进

改进方式:synchronized + Volatile 共同保证多线程安全

volatile保证 
1.多线程情况下对其他线程可见 
2.禁止指令重排
原理:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

synchronized为该方法加锁,与Volatile共同保证多线程安全

懒汉模式实现单例到这里就结束了吗?并没有

单例模式只需要创建一次实例,如果后面再次调用getInstance方法时,则直接返回之前创建的实例,可是现在我们的getInstance方法被加了锁,高并发情况下每次来调用此方法的时候,都需要进行锁竞争,效率很低,有没有什么办法可以保证我们的单例模式同时又提高效率呢?

那就是双重校验通过缩小synchronized的粒度,改善多线程下的单例对象的创建和获取的效率问题

至此,一个安全同时效率也不错的懒汉单例模式诞生了~~~

---------------------------嗝  吃了个午饭,我们接下来实现饿汉单例模式-------------------------------------------------------------------------------------------------------------------

实现二:饿汉单例

饿汉单例是实现一个安全的单例模式最简单粗暴的写法,之所以称为饿汉模式,是因为像饿汉一样,肚子很饿,想立马吃到东西,不想等待生产时间,这种写法,在类加载的时候九八Singleton实例给创建出来了
当然其缺点也很明显,可能在还不需要此实例的时候就已经把实例创建出来了,没有起到懒加载的效果

创建需要实现单例模式的类Person
由于类加载的时候就已经创建了Person的实例intance,不管谁调用getIntance方法都得到是intance,因此实现了单例和线程安全
怎么样,是不是很简单...

那么接下来,我们来个骚操作,枚举实现单例模式

----------------------------------------------------------------------------------------------------------------------------------------------

实现三:枚举实现单例模式

为什么我们可以用枚举来实现单例模式呢?
我们先来了解一下枚举,枚举是一种特殊的类,它继承自Enum类,可以重写方法,自定义属性等
同时它有一个最最最最重要的点,那就是枚举的构造方法只能由JVM虚拟机来调用!!!
也就是说,我们是无法通过构造方法来生产枚举类实例的,如果要生成,那么就在枚举类型内部自定义枚举实例

正是因为枚举有这些特性,我们可以通过枚举来实现单例模式

首先定义一个枚举类Worker,在其中只定义了一个ONLY实例

getInstance方法还是作为全局访问点返回ONLY同样的,测试方法中我们直接调用getInstance方法即可获得单例对象至此,通过枚举来实现单例也被我们收入囊中,今天真实收获满满的一天呢...

补充一下,静态内部类也可以实现枚举,此处不深究

---------------------------------------------------------------------------------------------------------------------------------------------

实现四:静态内部类

public class Singleton {

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

    private Singleton() {
        
    }

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

通过静态内部类的方式实现单例模式是线程安全的,同时静态内部类不会在Singleton类加载时就加载,而是在调用getInstance()方法时才进行加载,达到了懒加载的效果。

似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。且看如下代码:

public static void main(String[] args) throws Exception {
    Singleton singleton = Singleton.getInstance();
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton newSingleton = constructor.newInstance();
    System.out.println(singleton == newSingleton);
}

运行结果:

通过结果看,这两个实例不是同一个,这就违背了单例模式的原则了。

除了反射攻击之外,还可能存在反序列化攻击的情况。如下:

引入依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8.1</version>
</dependency>

这个依赖提供了序列化和反序列化工具类。

Singleton类实现java.io.Serializable接口。

如下:

public class Singleton implements Serializable {

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

    private Singleton() {

    }

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

    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        byte[] serialize = SerializationUtils.serialize(instance);
        Singleton newInstance = SerializationUtils.deserialize(serialize);
        System.out.println(instance == newInstance);
    }

}

运行结果:


她喜欢所以就做咯