01-单例模式(Singleton)

概述:

使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。

私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。

 

优点:

在内存里只有一个实例,减少了内存开销

可以避免对资源的多重占用

设置全局访问点,严格控制访问

缺点:

没有接口,拓展比较困难

 

饿汉式可以防止反射攻击,其他的则不能,因为饿汉式可以在类的链接阶段就构建好了静态字段(?)

 

一、懒汉式(线程不安全)

 

私有静态变量 uniqueInstance 被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。

 

这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (uniqueInstance == null) ,并且此时 uniqueInstance 为 null,那么会有多个线程执行 uniqueInstance = new Singleton(); 语句,这将导致实例化多次 uniqueInstance。

 

二、饿汉式(线程安全)

 

线程不安全问题主要是由于 uniqueInstance 被实例化多次,采取直接实例化 uniqueInstance 的方式就不会产生线程不安全问题。

但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。

 

三、懒汉式(线程安全)

 

只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 uniqueInstance。

但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 uniqueInstance 已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用。

 

四、双重校验锁(线程安全)

 

多例-Double Check

uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。

双重校验锁先判断 uniqueInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。

 

如果只使用了一个 if 语句。

if (uniqueInstance == null) {

    synchronized (Singleton.class) {

        uniqueInstance = new Singleton();

    }

}

在 uniqueInstance == null 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 uniqueInstance = new Singleton(); 这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if 语句。

 

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

1. 为 uniqueInstance 分配内存空间

2. 初始化 uniqueInstance

3. 将 uniqueInstance 指向分配的内存地址

 

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

 

五、静态内部类实现

注:私有构造函数非常重要,上面的那几种方式都要加,有空就加上

当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance()方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。(相当于内部类也是一个独立的类文件,但是只是在需要的时候才调用,具体时间就是调用其静态属性的时候)

这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。

为什么是 private static呢?为了避免其他类直接通过获取这个内部类来获取实例。

原理:

JVM在类的初始化阶段(即Class被加载后,并且被线程使用之前,都是类的初始化阶段)在这个阶段会执行类的初始化,在执行类初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化

 

六、枚举类实现

这种方式能够在类链接阶段的时候就已经构建好了INSTANCE对象,同时能够很好的防止反射攻击和序列化后失效的问题,具体原因看链接

关于枚举类为什么这么写

 

In JDK:

这儿就是线程安全的饿汉式

(这儿就没有防止反射攻击,可以自己创建一个Runtime实例)

 

In Spring:

AbstractFactoryBean 

#getObject()

这个就是非线程安全的懒汉式

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章