单例模式
单例模式(Singleton)是保证一个类只有一个实例,并且为它提供一个全局访问点。如果一个类在系统内使用的频率较高,则保证这个类在系统的生命周期内一直拥有唯一一个实例,减少该类在系统内频繁的创建与销毁对象,减少资源消耗。
UML图
单例的实现要点
单例模式要求类能够有返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法)。
单例的实现主要是通过以下三个步骤:
- 将类的构造方法定义为私有方法。这样其他类的代码就无法通过调用该类的构造方法来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例。
- 定义一个私有的类的静态实例。
-
单例追求的目标
线程安全。
- 懒加载。
-
代码实现
1、普通饿汉式
在类加载的时候就会生成实例对象,,后续通过 getInstance 可以获取对象。 ```java public class SingletonWithHungry {
//将构造方法设为私有 private SingletonWithHungry () {}
private static SingletonWithHungry singleton = new SingletonWithHungry();
//全局访问点,生成单例对象。 public static SingletonWithHungry newInstance() {
return singleton;
}
}
<a name="zAIib"></a>
#### 反射破坏单例
```java
public static SingletonWithHungry reflection(Class<?> clazz) {
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Object o = constructor.newInstance();
return (SingletonWithHungry) o;
}
解决办法:在单例类构造方法中抛出异常
private SingletonWithHungry () {
//需要if判断的原因是类在加载时,需要调用构造方法。
if (singleton != null)
throw new RuntimeException("非法创建操作!");
}
序列化破坏单例
当单例类实现Serializable接口时,就可被序列化破坏单例。
public static SingletonWithHungry serialize(SingletonWithHungry singleton) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(singleton);
ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
return (SingletonWithHungry)inputStream.readObject();
}
解决办法:
在单例类中加入readResolve方法,ObjectInputStream通过反射去调用readResolve方法,而我们写的类中,readResolve方法返回的是唯一的HungrySingleton类的实例,由此得到的对象和通过newInstance()得到的对象是一致的!
Unsafe破坏单例
2、枚举饿汉式
public enum SingletonWithEnum {
SINGLETEON;
}
enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,也为保证单例性做了一个铺垫。想要了解enum是如何工作的,就要对其进行反编译。反编译后就会发现,使用枚举其实和使用静态类内部加载方法原理类似。枚举会被编译成如下形式:
public final class SingletonWithEnum extends Enum{
//私有构造方法,这里调用了父类的构造方法,其中参数name对应了常量名,参数ordinal代表枚举的一个顺序
private SingletonWithEnum(String name, int ordinal)
{
super(name, ordinal);
}
//定义的枚举在这里声明了SingletonWithEnum常量对象引用
public static final SingletonWithEnum $VALUES;
//将所有枚举的实例存放在数组中
private static final SingletonWithEnum $VALUES[];
//对象的实例化在static静态块中
static
{
SingletonWithEnum = new SingletonWithEnum("SINGLETON", 0);
//将所有枚举的实例存放在数组中
$VALUES = (new SingletonWithEnum[] {
SINGLETON
});
}
}
其中,Enum是Java提供给编译器的一个用于继承的类。枚举量的实现其实是public static final T 类型的未初始化变量,之后,会在静态代码中对枚举量进行初始化。所以,如果用枚举去实现一个单例,这样的加载时间其实有点类似于饿汉模式,并没有起到lazy-loading的作用。
对于序列化和反序列化,因为每一个枚举类型和枚举变量在JVM中都是唯一的,即Java在序列化和反序列化枚举时做了特殊的规定,枚举的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法是被编译器禁用的,因此也不存在实现序列化接口后调用readObject会破坏单例的问题。并且反射也无法破坏反例模式。
3、普通懒汉式
懒汉式相对于饿汉式的优势是支持延迟加载。但缺点也很明显,因为使用了synchronized关键字导致这个方法的并发度很低。只有在第一次创建单例时才会发生线程安全问题,如果频繁地用到,就会导致性能瓶颈,这种实现方式就不可取了。
public class SingletonWithLazy {
private static SingletonWithLazy INSTANCE = null;
private SingletonWithLazy() {
}
public static synchronized SingletonWithLazy newInstance() {
if (INSTANCE == null) {
INSTANCE = new SingletonWithLazy();
}
return INSTANCE;
}
}
4、DCL懒汉式
当普通懒汉式使用synchronized后并发度很低,并且只有在第一次创建单例时才可能发生线程安全问题,这时,我们采用双重检查锁机制(DCL,即 double-checked locking)。
public class SingletonWithDCL {
private static volatile SingletonWithDCL INSTANCE = null;
private SingletonWithDCL() {}
public static SingletonWithDCL newInstance() {
if (INSTANCE == null) {
synchronized (SingletonWithDCL.class) {
if (INSTANCE == null) {
INSTANCE = new SingletonWithDCL();
}
}
}
return INSTANCE;
}
}
成员变量加volatile关键字是为了防止指令重排。指令重排是指计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。在INSTANCE = new SingletonWithDCL()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1. 给 singleton 分配内存
2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null了)
执行顺序可能是1-2-3也可能是1-3-2。当有红蓝两个线程时,他们的执行顺序如图所示,红色线程已经返回结果,但在蓝色线程中还未调用构造方法初始化,就会出现各种不可预知的问题。
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
I. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
II. 它会强制将对缓存的修改操作立即写入主存;
III. 如果是写操作,它会导致其他CPU中对应的缓存行无效。
被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。把instance声明为volatile之后,对它的写操作就会有一个内存屏障,这样,在它的赋值完成之前,就不用会调用读操作。
5、静态内部类
public class SingletonWithInnerClass implements Serializable {
private SingletonWithInnerClass() {}
//私有内部静态类
private static class Holder {
static SingletonWithInnerClass INSTANCE;
static {
INSTANCE = new SingletonWithInnerClass();
}
}
public static SingletonWithInnerClass newInstance() {
return Holder.INSTANCE;
}
}
静态内部类(延迟初始化占位类),比较常见的一种写法。JVM将推迟Holder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化INSTANCE,因此不需要额外的同步。当任何一个线程第一次调用newInstance时,都会使Holder被加载和被初始化,此时静态初始化器将执行INSTANCE的初始化操作。
通过静态初始化来初始化INSTANCE为什么不需要额外的同步?
在初始器中采用了特殊的方式来处理静态域(或者在静态初始化代码块中初始化的值),并提供了额外的线程安全性保证。静态初始化器是由JVM在类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显式的同步。然而,这个规则仅适用于在构造时的状态,如果对象是可变的,那么在读线程和写线程之间仍然需要通过同步来确保随后的修改操作是可见的,以及避免数据破坏。
总结
几种方式的比较:
方式 | 优点 | 缺点 |
---|---|---|
饿汉式 | 简单、线程安全、效率高 | 不支持懒加载 |
枚举(推荐) | 线程安全、效率高、反破坏强 | 不支持懒加载 |
懒汉式 | 线程安全、懒加载 | 效率低 |
双重检查锁机制 | 线程安全、懒加载、效率高 | 无 |
静态内部类(推荐) | 线程安全、懒加载、效率高 | 无 |