创建型模式之单例模式
1 概述
单例模式应该是最简单,同时又是最复杂的一种创建型模式。因为大家都知道这个模式:无非就是保证某个对象在系统中只存在一个实例。然而想要真正实现一个完美的单例模式,却不简单。
2 单例模式
一般单例模式的实现,都需要包含两个步骤:
- 将类的构造函数私有化。
- 提供一个
public
的方法,以供外界获取唯一的实例。
下面将一一介绍单例模式的各种实现方式。
3 案例
3.1 注册表式
提供一个注册表类,来维护所有单例的实例
public class Test { public static void main(String[] args) {
SampleClass singleton1 = Registry.getInstance(SampleClass.class);
SampleClass singleton2 = Registry.getInstance(SampleClass.class);
System.out.println("Registry singleton instance1: " + singleton1.hashCode());
System.out.println("Registry singleton instance2: " + singleton2.hashCode());
System.out.println("We can broke singleton by new a instance through class"s construct method");
SampleClass singleton3 = new SampleClass();
System.out.println("Registry singleton instance3: " + singleton3.hashCode());
}
}
public class Registry {
private static Map<Class, Object> registry = new ConcurrentHashMap<>();
private Registry() {};
public static synchronized <T> T getInstance(Class<T> type) {
Object obj = registry.get(type);
if (obj == null) {
try {
obj = type.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
registry.put(type, obj);
}
return (T) obj;
}
}
public class SampleClass {
}
输出:
Registry singleton instance1: 21685669Registry singleton instance2: 21685669
We can broke singleton by new a instance through class"s construct method
Registry singleton instance3: 2133927002
用注册表实现的单例其实是伪单例,因为它只能保证从注册表中获取的对象是全局唯一的。如果我们不从注册表获取,而是直接new
一个实例,这显然破坏了单例模式。我们熟悉的Spring
框架,就是用这种模式实现的单例,其中的Registry
就是BeanFactory
。
要从根本上实现实例的全局唯一,我们必须在单例类本身下功夫。
3.1 饿汉式----静态属性
将实例作为类的一个静态变量,来实现唯一性:
public class StaticFieldTest { public static void main(String[] args) {
StaticFieldSingleton fieldSingleton1 = StaticFieldSingleton.getInstance();
StaticFieldSingleton fieldSingleton2 = StaticFieldSingleton.getInstance();
System.out.println("StaticFieldSingleton instance1: " + fieldSingleton1.hashCode());
System.out.println("StaticFieldSingleton instance1: " + fieldSingleton2.hashCode());
}
}
public class StaticFieldSingleton {
private static StaticFieldSingleton singletonInstance = new StaticFieldSingleton();
// 将构造方法私有化
private StaticFieldSingleton(){};
// 提供唯一的接口,供外部获取唯一的变量
public static StaticFieldSingleton getInstance() {
return singletonInstance;
}
}
输出:
StaticFieldSingleton in multi-thread instance: 837048303StaticFieldSingleton in multi-thread instance: 837048303
当类StaticFieldSingleton
被加载进JVM
的时候,类的实例会作为类的静态属性,随着类一起初始化。这种实现方式其实是依靠类加载器来保证实例的唯一性。优点是,不需要考虑多线程加锁,实现起来比较简单。缺点是,无论后续是否会用到,实例都会在class
被加载的时候被创建好。这对于内存资源比较宝贵的场景,或者目标是某些如File System
的大对象的时候,会导致资源的浪费。同时,这种方式也无法提供对异常的处理,在某些情况下,会导致程序出错。
3.2 饿汉式----静态块
将类实例的初始化放在类的静态块中:
public class StaticBlockTest { public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
StaticBlockSingleton staticBlockSingleton = StaticBlockSingleton.getInstance();
System.out.println("StaticBlockSingleton in multi-thread instance: " + staticBlockSingleton.hashCode());
}).start();
}
}
}
public class StaticBlockSingleton {
private static StaticBlockSingleton singletonInstance;
private StaticBlockSingleton(){};
// 静态块会在类被加载进内存的时候被执行
static {
try {
singletonInstance = new StaticBlockSingleton();
} catch (Exception e) {
e.printStackTrace();
}
}
public static StaticBlockSingleton getInstance() {
return singletonInstance;
}
}
输出:
StaticBlockSingleton in multi-thread instance: 2132107705StaticBlockSingleton in multi-thread instance: 2132107705
静态块中初始化与静态变量上初始化本质上是一样的,都是通过类加载器来保证实例只会被初始化一次。区别是,静态块初始化可以做异常的捕获与处理,同时还允许我们在静态块中做一些额外的事情,比静态变量的方式更自由。
但两种饿汉式都不可避免地会造成额外内存的占用,于是出现了按需加载的懒汉式创建方式。
3.3 懒汉式----基础版
将类实例的初始化放在方法中。只有当方法第一次被访问的时候,去初始化实例:
public class SynchronizedTest { public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
SynchronizedSingleton synchronizedSingleton = SynchronizedSingleton.getInstance();
System.out.println("SynchronizedSingleton in multi-thread instance: " + synchronizedSingleton.hashCode());
}).start();
}
}
}
public class SynchronizedSingleton {
private static SynchronizedSingleton singletonInstance;
private SynchronizedSingleton(){};
// 加了同步锁,保证new SynchronizedSingleton()只会被第一个线程访问
public static synchronized SynchronizedSingleton getInstance() {
if (singletonInstance == null) {
singletonInstance = new SynchronizedSingleton();
}
return singletonInstance;
}
}
输出:
SynchronizedSingleton in multi-thread instance: 554449003SynchronizedSingleton in multi-thread instance: 554449003
懒汉式解决了饿汉式存在的最大问题:可能导致的内存浪费。只有当getInstance()
方法第一次被访问的时候,实例才会去真正创建。而方法上加了synchronized
,保证了后续对方法的访问,都只会返回之前创建好的实例,保证了唯一性。
这种方式的不足是,每次对getInstance()
方法的访问,都需要获取锁,众所周知,锁的获取与释放是一笔昂贵的开销。而事实上只有当第一次实例创建的时候需要加锁。于是有了改进的方式:双检锁。
3.4 懒汉式----双检锁
双检锁(Double Check Lock)是一个很多人都熟悉的概念,是上述模式的增强版。实现如下:
public class DoubleCheckLockTest { public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
DoubleCheckLockSingleton doubleCheckLockSingleton = DoubleCheckLockSingleton.getInstance();
System.out.println("DoubleCheckLockSingleton in multi-thread instance: " + doubleCheckLockSingleton.hashCode());
}).start();
}
}
}
public class DoubleCheckLockSingleton {
// 变量必须声明为volatile,否则可能会得到一个“半初始化”的实例
private static volatile DoubleCheckLockSingleton singletonInstance;
private DoubleCheckLockSingleton(){};
// 若实例已经被创建,则不需要再进入同步块
// 若实例还没创建,则在同步块中检查并创建实例
public static DoubleCheckLockSingleton getInstance() {
DoubleCheckLockSingleton instance = singletonInstance;
if (instance == null) {
synchronized (DoubleCheckLockSingleton.class) {
instance = singletonInstance;
if (instance == null) {
instance = singletonInstance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
}
输出:
DoubleCheckLockSingleton in multi-thread instance: 837048303DoubleCheckLockSingleton in multi-thread instance: 837048303
单例模式中,水最深的应该就是是双检锁了。在上述实现中,有几个要点:
- 为什么需要两次
if
检查:第一次if
检查在synchronized
块之外,当实例已经被创建好之后,可以立即返回。第二次if
检查,是因为在高并发的情况下,可能会有好多线程走到第一个if
块中,去争抢synchronized
锁,我们必须保证只有第一个抢到锁的线程能创建实例,所以后面的线程必须再进行一次if
判断,发现实例已经被第一个抢到锁的线程初始化好了,直接返回该实例。这也是双检名字的由来。 - 为什么成员变量
singletonInstance
要声明为volatile
:因为new DoubleCheckLockSingleton()
其实并不是一个原子操作,主要可以分为给实例分配堆内存,执行类的构造函数,将实例引用赋给调用者三步。而由于重排序的存在,在某一些机器上,第三步会先于第二步发生,于是可能出现,线程A走到了new DoubleCheckLockSingleton()
,但并未执行完构造函数时,线程B发现instance != null
了,于是对instance
的属性进行访问,结果看到的属性都是默认值。而JMM
在Java1.5
之后进行了增强,volatile
关键字可以禁止编译器的重排序,并会在volatile
关键字修饰的变量前后适当位置添加内存屏障,保证程序不会读到半初始化的实例。关于JMM
的增强,可以扩展阅读Doug Lea大神的文章。 - 为什么要加局部变量
instance
:加这个局部变量,主要是为了提高程序的性能。因为成员变量singletonInstance
是声明为volatile
的,而所有对volatile
变量的操作(读写)都必须与主内存交互,开销相对较大。加局部变量可以减少与volatile
变量的交互。这也是java.util.concurrent
包中很多工具类的常见做法。
到这里,似乎双检锁的方案已经很完美了,确实,这也是被很多人所采用的单例模式实现方案。但其实懒汉式还有一种更为通用的实现方式。
3.5 懒汉式----静态内部类
引入一个静态内部类,来实现对静态变量的延迟加载:
public class InnerClassWrappedTest { public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
InnerClassWrappedSingleton innerClassWrappedSingleton = InnerClassWrappedSingleton.getInstance();
System.out.println("InnerClassWrappedSingleton in multi-thread instance: " + innerClassWrappedSingleton.hashCode());
}).start();
}
}
}
public class InnerClassWrappedSingleton {
private InnerClassWrappedSingleton(){};
// 内部类持有单例,仅当getInstance()方法被调用的时候,SingletonHolder类才会被加载
// final关键字保证了不会得到“半初始化”的实例
private static class SingletonHolder {
private static final InnerClassWrappedSingleton instance = new InnerClassWrappedSingleton();
}
public static InnerClassWrappedSingleton getInstance() {
return SingletonHolder.instance;
}
}
输出:
InnerClassWrappedSingleton in multi-thread instance: 2132107705InnerClassWrappedSingleton in multi-thread instance: 2132107705
上述实现其实可以看作是饿汉式----静态块的升级版,只不过把实例的初始化,放到了静态内部类中。而该静态内部类只有在getInstance()
被调用的时候,才会被加载,从而对单例进行初始化。同样,由类加载器保证了,只有一个实例会被创建。同时,final
关键字在Java1.5
之后也进行了增强,可以保证得到的一定是一个完整的单例。
这种方式是本人觉得比较好的方式,因为实现简单线程安全,而且适用性很强。
3.6 破坏单例----序列化
其实所有上述的实现方式,都不可能完全保证类的唯一,因为尽管我们把类的构造器设为了private
,但仍然有办法用其他方式创建新的实例。比如不巧,单例的类正好实现了Serializable
接口,那么黑客们可以通过序列化的方式,得到一个新的“单例”:
public class SingletonDestroyerSerialization { public static void main(String[] args) throws IOException, ClassNotFoundException {
InnerClassWrappedSingleton instance1 = InnerClassWrappedSingleton.getInstance();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
// 将单例序列化
oos.writeObject(instance1);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
// 反序列化,创建一个新的“单例”
InnerClassWrappedSingleton instance2 = (InnerClassWrappedSingleton) ois.readObject();
System.out.println("singleton instance1: " + instance1.hashCode());
System.out.println("singleton instance2: " + instance2.hashCode());
}
}
public static class InnerClassWrappedSingleton implements Serializable {
private InnerClassWrappedSingleton(){};
private static class SingletonHolder {
private static final InnerClassWrappedSingleton instance = new InnerClassWrappedSingleton();
}
public static InnerClassWrappedSingleton getInstance() {
return SingletonHolder.instance;
}
}
输出:
singleton instance1: 1173230247singleton instance2: 764977973
显然,输出了不同的hashcode
,JVM
中存在了两个“单例”对象。
为了防止以上情况出现,我们可以在单例类中,添加一个readResolve()
方法,并返回单例实例。这样,在反序列化之后,我们得到的依然是原先的实例:
public static class InnerClassWrappedSingleton implements Serializable { private InnerClassWrappedSingleton(){};
private static class SingletonHolder {
private static final InnerClassWrappedSingleton instance = new InnerClassWrappedSingleton();
}
public static InnerClassWrappedSingleton getInstance() {
return SingletonHolder.instance;
}
// 添加此方法,防止序列化与反序列化创建新的实例
private Object readResolve() {
return SingletonHolder.instance;
}
}
3.7 破坏单例----反射
如果说序列化与反序列化我们还有应对的办法,那么对于反射攻击,上述所有的实现方案,都无可奈何:
public class SingletonDestroyerRefelct { public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException {
InnerClassWrappedSingleton instance1 = InnerClassWrappedSingleton.getInstance();
System.out.println("singleton instance1: " + instance1.hashCode());
Constructor[] constructors = InnerClassWrappedSingleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
// 利用反射,创建一个新的“单例”变量
constructor.setAccessible(true);
InnerClassWrappedSingleton instance2 = (InnerClassWrappedSingleton) constructor.newInstance();
System.out.println("singleton instance2: " + instance2.hashCode());
break;
}
}
}
public static class InnerClassWrappedSingleton {
private InnerClassWrappedSingleton(){};
private static class SingletonHolder {
private static final InnerClassWrappedSingleton instance = new InnerClassWrappedSingleton();
}
public static InnerClassWrappedSingleton getInstance() {
return SingletonHolder.instance;
}
}
输出:
singleton instance1: 1735600054singleton instance2: 21685669
反射其实类似Java
中的一个后门,非常强大,它能破坏单例模式也是情理之中。类似JSON
序列化与反序列化,也能创建多个不同的“单例”,利用的也是反射机制。
3.8 究极单例----Enum
有没有办法防止反射调用破坏单例呢?答案是肯定的,即用enum
创建单例:
public enum EnumSingleton { INSTANCE;
public void doSomething() {
}
}
首先,JVM
对enum
型变量的序列化与反序列化做了特殊处理,保证反序列化之后得到的依然是内存中的那个enum
。
第二,Java从语言层面保证,无法通过反射创建enum
类型变量。
所以,如果说要选一种最安全的单例模式实现方案,那非Enum模式莫属。这也是「Effective Java」的作者Joshua Bloch所推荐的方式。
4 总结
本文介绍了形形色色很多的单例模式,其实也并不是越到后面的实现越好,而是要看每个版本的特性,选择最适合自己项目的那个版本。
文中例子的github地址
以上是 创建型模式之单例模式 的全部内容, 来源链接: utcz.com/z/514854.html