概述:
使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。
私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。
优点:
在内存里只有一个实例,减少了内存开销
可以避免对资源的多重占用
设置全局访问点,严格控制访问
缺点:
没有接口,拓展比较困难
饿汉式可以防止反射攻击,其他的则不能,因为饿汉式可以在类的链接阶段就构建好了静态字段(?)
一、懒汉式(线程不安全)
私有静态变量 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()
这个就是非线程安全的懒汉式