单例模式
单例模式用来保证一个对象在运行期间只会被创建一次,通常,单例类会提供一个类静态方法
getInstance提供该类的唯一实例。
单例模式只需要保证类实例只会被创建一次,所以单例模式有多种实现。
饿汉模式
@ThreadSafe
public class Singleton {
// 提前初始化,未被使用则造成资源浪费
private static Singleton instance = new Singleton();
// 使其它类无法创建新对象
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
饿汉模式在类初始化时就创建了对象,在调用 getInstance 的方法时直接返回该对象即可,不需要再去创建该对象的实例。但是如果该类没有被使用到,则实例占用的空间就浪费了,这种方式也算是用空间换时间。
懒汉模式
非线程安全的懒汉模式
@NotThreadSafe
public class Singleton {
private static Singleton instance = new Lazy();
// 使其它类无法创建新对象
private Singleton() {
}
// 非线程安全
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方式将实例的创建延迟到第一次调用 getInstance 方法时,如果该类没有被使用到,不会造成资源浪费。
但是在并发访问时,可能会出现多个线程同时进入 if 判断,又同时创建了对象的情况发生,所以非线程安全,不可取。
普通懒汉模式
@ThreadSafe
public class Singleton {
private static Singleton instance = null;
// 使其它类无法创建新对象
private Singleton() {
}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
这里在对象创建前加锁,避免出现多线程并发访问时创建多个对象的意外。这种写法相当于在方法上加锁,这种写法主要是方便与双重检测锁写法对比。
这种实现的主要问题在于,getInstance 方法每次被调用时,都需要获取锁,当有大量线程访问时,会出现很多线程被阻塞的情况,影响性能。而实际上,实例被初始化之后,直接返回被初始化的对象即可,不会有线程安全问题。
双重检测锁
@ThreadSafe
public class DoubleCheckLock {
// volatile 禁止重排序,保证对 instance 操作可见性
private volatile static DoubleCheckLock instance = null;
// 使其它类无法创建新对象
private DoubleCheckLock() {
}
public static DoubleCheckLock getInstance() {
if (instance == null) {//①
synchronized (DoubleCheckLock.class) {//②
if (instance == null) {//③
instance = new DoubleCheckLock();//④
}
}
}
return instance;
}
}
第①步的判断,是对于上述普通懒汉模式的优化。在实例已经被初始化之后,调用 getInstance 方法,不再需要获取同步状态。
第②步,是在实例的创建入口加锁保证线程安全。
第③步,是保证阻塞在第②步的线程获取锁之后,重新检测一下实例是否已经被初始化,避免二次创建。
第④步,是创建实例的过程。
与普通懒汉模式相比,双重检测锁实现不仅是多了第①步,类静态属性 instance 也多了一个修饰符 volatile。
volatile 的作用是防止第④步创建实例时的重排序,以及保证 instance 可见性。
重新分析第④步 instance = new DoubleCheckLock(),这并不是原子操作,它可以被拆分成:
- 堆内存开辟空间准备初始化对象
- 初始化对象
- 栈中引用
instance指向堆内存空间地址
三步操作中,2 与 3 都依赖 1, 2 和 3 无依赖关系,因此 2 和 3 是可以被优化重排序的。
假设当线程 A 创建实例时,出现了重排序的情况,1 之后执行了 3,而暂未执行 2 时,会出现 instance != null 的情况,而此时实例未被初始化。此时若有线程 B 调用了 getInstance 方法,会直接返回 instance,当线程 B 该实例操作时,会抛出 NullPointerException,之后线程 A 才执行 2。
此处加上 volatile 修饰 instance ,可以保证第④步不会被重排序,同时 instance 的指向更新后能立马被其它线程看到,避免了该问题的发生。
在单线程中,即使 2 和 3 被重排序了,也一定是 2 执行后才会执行后续操作,所以不会出现该问题。
静态内部类
@ThreadSafe
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
由于 JVM 只在特定的 5 种场景(类的主动引用)下才会对类初始化,而静态内部类不在其中,称为被动引用。只有当他被其它操作访问时,才会被初始化。所以静态内部类是延迟加载的。
而静态内部类的 instance 实例初始化过程是由 JVM 来保证线程安全的。《深入理解 Java 虚拟机》中描述如下:
虚拟机会保证一个类的
<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行
< clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞 (需要注意的是,其他线程虽然会被阻塞,但如果执行< clinit>()方法后,其他线程唤醒之后不会再次进入< clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
因此静态内部类保证了单例的唯一性,同时也延迟了单例的实例化。
静态内部类的唯一缺陷在于静态内部类创建对象时,无法接收到 getInstance 方法传递的参数。当需要根据参数创建对象时,可以选择使用 DCL。
反射问题
上述单例模式的实现,都可以用反射来破坏单例特性。
public class ReflectionCase {
public static void main(String[] args) throws Exception {
// 静态内部类实现单例
Singleton instance = Singleton.getInstance();
Singleton newInstance;
Class<Singleton> singletonClass = Singleton.class;
Constructor constructor = singletonClass.getDeclaredConstructor();
constructor.setAccessible(true);
newInstance = (Singleton) constructor.newInstance();
System.out.println(instance == newInstance);
}
}
该测试运行后打印的结果为 false,说明已经产生了两个对象,单例被破坏。
序列化问题
使静态内部类实现中的 Singleton 类实现序列化接口。
public class SerializationCase {
public static void main(String[] args) throws Exception {
// 静态内部类实现单例
Singleton instance = Singleton.getInstance();
Singleton newInstance;
// 序列化
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
outputStream.writeObject(instance);
outputStream.close();
// 反序列化
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("singleton.ser"));
newInstance = (Singleton) inputStream.readObject();
inputStream.close();
System.out.println(instance == newInstance);
}
}
运行该程序,打印结果为 false,同样说明已经产生了两个对象,单例被破坏。
克隆问题
@NotThreadSafe
public class Singleton implements Cloneable{
// 静态内部类实现单例
...
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class CloneCase {
public static void main(String[] args) throws CloneNotSupportedException {
// 静态内部类实现单例
Singleton instance = Singleton.getInstance();
Singleton newInstance = (Singleton) instance.clone();
System.out.println(instance == newInstance);
}
}
在这种情况下,打印的结果为 false。
此时枚举类实现的单例都能被破坏。因为 clone 方法在 Object 类中的实现是会将内存中的对象拷贝一份生成新对象,所以必然会产生新对象,即使是枚举单例也会被破坏。因此,需要保证单例的类不会实现 Cloneable 接口。只要满足该条件,用枚举实现的单例就是绝对安全的。
枚举类
@ThreadSafe
public enum EnumSingleton {
INSTANCE;
}
-
该类在编译之后类的声明会变为
public final class EnumSingleton extends Enum,枚举值INSTANCE也会被声明为public static final EnumSingleton INSTANCE。因此枚举类实现的单例是线程安全的。 -
枚举类的构造由 JVM 控制,无法被我们直接构造。在用反射破坏静态内部类实现的单例模式时, 调用的
java.lang.reflect.Constructor的newInstance方法中,对ENUM有特别的标注:if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");所以枚举类单例无法被序列化破坏。
-
枚举类序列化后的内容只包括枚举类的
name;反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的修改。在序列化与反序列化过程中,枚举类中定义的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法都会被忽略。所以枚举类实现的单例也不会被序列化破坏。
在 《Effective Java》 中也提到:
使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现
Singleton的最佳方法。
枚举单例的缺点在于所有的属性都必须在创建时指定, 也就意味着不能延迟加载; 并且使用枚举时占用的内存比静态变量的 2 倍还多, 这在性能要求严苛的应用中是不可忽视的.
总结
在实际开发中,使用双重检测锁 、静态内部类以及枚举类实现单例都是可以的。