一、含义
“单例”的书面含义是:在一个Java进程中,某个给定类的实例只允许存在一个。
二、实现方案
实现“单例”方案须满足两个核心目标:
- 的确是“单例”
- 线程安全
常见的单例实现方案有6种,具体可细分为3类:静态初始化锁机制,内置锁/高级锁,枚举。
接下来基于以下几个维度对上述6种单例实现方案进行评价:
- 实现可读性,用5分制来进行评价
- 性能
- 是采用“懒汉模式(在获取单例实例时才生成)”还是“饿汉模式(在获取单例实例前已生成)”
2.1、静态初始化锁机制
利用静态初始化锁机制来获得“确保单例”和“线程安全”。
1、方案1
package single;
public class Singleton1 {
private static Singleton1 instance = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return instance;
}
}
实现可读性 | 性能 | 懒汉模式/饿汉模式 |
---|---|---|
5 | 好 | 饿汉模式 |
2、方案2
package single;
public class Singleton2 {
private static Singleton2 instance;
static {
instance = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance() {
return instance;
}
}
一个简单变种。
实现可读性 | 性能 | 懒汉模式/饿汉模式 |
---|---|---|
5 | 好 | 饿汉模式 |
3、方案3
package single;
public class Singleton3 {
private Singleton3() {
}
public static Singleton3 getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final Singleton3 INSTANCE = new Singleton3();
}
}
采用懒汉模式,代价是损失一点可读性。
实现可读性 | 性能 | 懒汉模式/饿汉模式 |
---|---|---|
4 | 好 | 懒汉模式 |
2.2、内置锁/高级锁
利用“内置锁(即synchronized锁)”或者高级锁来获得“线程安全”,利用条件判断获得“确保单例”。
4、方案4
package single;
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
public static synchronized Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
每次获取单例都需要申请锁和释放锁,大部分情况是不必要的,性能较差。
实现可读性 | 性能 | 懒汉模式/饿汉模式 |
---|---|---|
5 | 差 | 懒汉模式 |
5、方案5
package single;
public class Singleton5 {
private static Singleton5 instance;
private static volatile boolean init = false;
private Singleton5() {
}
public static synchronized Singleton5 getInstance() {
if (!init) {
synchronized (Singleton5.class) {
if (!init) {
instance = new Singleton5();
init = true;
}
}
}
return instance;
}
}
经典的双重检查锁定方式,“init”变量需要用volatile
修饰的原因在于:否则,由于重排序/狭义可见性,外层的if (!init)
语句返回false时,“instance”变量可能还未完成初始化。
实现可读性 | 性能 | 懒汉模式/饿汉模式 |
---|---|---|
5 | 好 | 懒汉模式 |
在以下简单变种中,“instance”变量仍然需要用volatile
修饰的原因在于:否则,由于重排序的存在,外层的if (instance == null)
返回false时,“instance”变量可能还未完成初始化,只不过是由于重排序先返回了变量引用而已。
package single;
public class Singleton55 {
private static volatile Singleton55 instance;
private Singleton55() {
}
public static synchronized Singleton55 getInstance() {
if (instance == null) {
synchronized (Singleton55.class) {
if (instance == null) {
instance = new Singleton55();
}
}
}
return instance;
}
}
2.3、枚举
利用枚举自身的语法特性,来获得“确保单例”和“线程安全”。
6、方案6
package single;
public enum Singleton6 {
SINGLE;
char c;
double d;
Singleton6() {
init();
}
public static Singleton6 getInstance() {
return SINGLE;
}
private void init() {
c = 'a';
d = 20D;
}
}
实现可读性 | 性能 | 懒汉模式/饿汉模式 |
---|---|---|
3 | 好 | 饿汉模式 |
三、反单例攻击
“反单例攻击”含义:通过某种手段,在“单例”之外创建生成新实例。
常见的反单例攻击手段有:
- 使用
new + 构造方法
创建新实例 - 使用
克隆
创建新实例 - 使用
反射
创建新实例 - 使用
序列化-反序列化
创建新实例 - 使用
多类加载器加载
创建新实例
3.1、new + 构造方法
构造方法暴露给外部,使得外部可直接使用new + 构造方法
的形式创建新实例。
实验代码1:
enum C {
SINGLE;
char c;
double d;
C() {
init();
}
public static C getInstance() {
return SINGLE;
}
private void init() {
c = 'a';
d = 20D;
}
}
class A {
private static A instance = new A();
public A() {
}
public static A getInstance() {
return instance;
}
}
class B {
private static B instance;
private static volatile boolean init = false;
public B() {
}
public static synchronized B getInstance() {
if (!init) {
synchronized (B.class) {
if (!init) {
instance = new B();
init = true;
}
}
}
return instance;
}
}
public class NewConstructorAttack {
public static void main(String[] args) {
System.out.println(A.getInstance() == new A());
System.out.println(B.getInstance() == new B());
//不被允许
//new C();
}
}
结论:
- 由于枚举类不支持使用
new + 构造方法
的形式创建新实例,因此方案6天然防御该形式攻击 - 方案1-5为防御
new + 构造方法
攻击,可将构造方法设为由private
修饰
3.2、克隆
类继承java.lang.Cloneable
接口,并覆盖实现clone()
方法,调用clone()
方法可以创建生成一个新的实例。
实验代码2:
enum F implements Cloneable {
SINGLE;
char c;
double d;
F() {
init();
}
public static F getInstance() {
return SINGLE;
}
private void init() {
c = 'a';
d = 20D;
}
// 不允许覆盖实现Object类的clone()方法
// @Override
// public Object clone() {
//
// }
}
class D implements Cloneable {
private static D instance = new D();
private D() {
}
public static D getInstance() {
return instance;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class E implements Cloneable {
private static E instance;
private static volatile boolean init = false;
private E() {
}
public static synchronized E getInstance() {
if (!init) {
synchronized (E.class) {
if (!init) {
instance = new E();
init = true;
}
}
}
return instance;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class CloneAttack {
public static void main(String[] args) throws CloneNotSupportedException {
D d = D.getInstance();
D dClone = (D) d.clone();
System.out.println(d == dClone);
E e = E.getInstance();
E eClone = (E) e.clone();
System.out.println(e == eClone);
// F实例不能访问clone()方法
}
}
结论:
- 枚举类不能覆盖实现
clone()
方法,因此方案6天然防御该形式攻击 - 方案1-5为防御
克隆
攻击,可禁止继承java.lang.Cloneable
接口,或者在继承的情况下,在覆盖实现的clone()
方法中返回已经生成的实例,而不是重新生成,类似如下代码
@Override
public Object clone() throws CloneNotSupportedException {
return instance;
}
3.3、反射
通过反射获取Constructor
对象,然后调用newInstance()
方法生成实例。
实验代码3:
import java.lang.reflect.Constructor;
enum L {
SINGLE;
char c;
double d;
L() {
init();
}
public static L getInstance() {
return SINGLE;
}
private void init() {
c = 'a';
d = 20D;
}
}
class J {
private static J instance = new J();
private J() {
}
public static J getInstance() {
return instance;
}
}
class K {
private static K instance;
private static volatile boolean init = false;
private K() {
}
public static synchronized K getInstance() {
if (!init) {
synchronized (E.class) {
if (!init) {
instance = new K();
init = true;
}
}
}
return instance;
}
}
public class ReflectionAttack {
public static void main(String[] args) throws Exception {
Constructor jConstructor = J.class.getDeclaredConstructor();
jConstructor.setAccessible(true);
//返回false
System.out.println(J.getInstance() == jConstructor.newInstance());
// ---分割线---
Constructor kConstructor = K.class.getDeclaredConstructor();
kConstructor.setAccessible(true);
//返回false
System.out.println(K.getInstance() == kConstructor.newInstance());
// ---分割线---
// 枚举类的默认构造方法有“String和int”两个参数
Constructor lConstructor = L.class.getDeclaredConstructor(String.class, int.class);
lConstructor.setAccessible(true);
System.out.println(L.getInstance() == lConstructor.newInstance("AnotherInstance", 1000));
}
}
结论:
- 对于枚举类,在获得其
Constructor
对象,然后调用newInstance()
方法时会直接抛出java.lang.IllegalArgumentException
异常,查看方法实现源码可知“枚举类直接被限定不能调用该方法”,因此方案6天然防御该形式攻击 - 方案1-5为防御
反射
攻击,对于“饿汉方案”,可在构造方法中增加“重复生成则抛出异常”的判断逻辑,类似如下代码;而对于“懒汉方案”,则无能为力
if (instance != null) {
throw new RuntimeException("the instance exists");
}
3.4、序列化-反序列化
序列化存在多种形式,这里讨论常见的JDK序列化和JSON序列化(以Fastjson序列化为例)。
实验代码4:
import com.alibaba.fastjson.JSON;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
enum I implements Serializable {
SINGLE;
char c;
double d;
I() {
init();
}
public static I getInstance() {
return SINGLE;
}
private void init() {
c = 'a';
d = 20D;
}
}
class G implements Serializable {
private static G instance = new G();
private G() {
}
public static G getInstance() {
return instance;
}
}
class H implements Serializable {
private static H instance;
private static volatile boolean init = false;
private H() {
}
public static synchronized H getInstance() {
if (!init) {
synchronized (E.class) {
if (!init) {
instance = new H();
init = true;
}
}
}
return instance;
}
}
public class DeserializationAttack {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
jdkDeserializationAttack();
fastjsonDeserializationAttack();
}
private static void jdkDeserializationAttack() {
G g = G.getInstance();
G anotherG = (G) serializeAndDeserializeObjectJDK(g);
//返回false
System.out.println(g == anotherG);
// ---分割线---
H h = H.getInstance();
H anotherH = (H) serializeAndDeserializeObjectJDK(h);
//返回false
System.out.println(h == anotherH);
// ---分割线---
I i = I.getInstance();
I anotherI = (I) serializeAndDeserializeObjectJDK(i);
//返回true
System.out.println(i == anotherI);
}
private static Object serializeAndDeserializeObjectJDK(Serializable serializable) {
try {
ByteArrayOutputStream oo = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(oo);
oos.writeObject(serializable);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(oo.toByteArray()));
return ois.readObject();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static void fastjsonDeserializationAttack() {
G g = G.getInstance();
G anotherG = (G) serializeAndDeserializeObjectFastjson(g, G.class);
//返回false
System.out.println(g == anotherG);
// ---分割线---
H h = H.getInstance();
H anotherH = (H) serializeAndDeserializeObjectFastjson(h, H.class);
//返回false
System.out.println(h == anotherH);
// ---分割线---
I i = I.getInstance();
I anotherI = (I) serializeAndDeserializeObjectFastjson(i, I.class);
//返回true
System.out.println(i == anotherI);
}
private static Object serializeAndDeserializeObjectFastjson(Object obj, Class clz) {
try {
String jsonStr = JSON.toJSONString(obj);
return JSON.parseObject(jsonStr, clz);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
3.4.1、JDK序列化
类继承java.lang.Serializable
接口,通过对一个实例序列化和反序列化,得到一个新的实例。
结论:
- JDK序列化/反序列化对枚举类特殊处理,经过
序列化-反序列化
过程,不会生成一个新的实例,因此方案6天然防御该形式攻击 - 方案1-5为防御
JDK序列化
攻击,可禁止继承java.io.Serializable
接口;或者在继承的情况下,覆盖实现readResolve()
方法,在该方法中直接返回已经生成的实例。另外需要特别注意的一点是,JDK的反序列化过程无需调用构造方法,因此,不能采用“Fastjson序列化攻击防御”中的“排除定义无参构造方法”方案
3.4.2、Fastjson序列化
通过对一个实例进行Fastjson序列化和反序列化,得到一个新的实例。
结论:
- Fastjson序列化/反序列化对枚举类特殊处理,经过
序列化-反序列化
过程,不会生成一个新的实例,因此方案6天然防御该形式攻击 - 方案1-5为防御
Fastjson序列化
攻击,可采用“排除定义无参构造方法(比如显式定义一个有参构造方法)”方案,本质原因在于Fastjson反序列化时需要调用无参构造方法
3.5、多类加载器加载
多个类加载器加载生成多个不同的Class对象,再生成多个不同的实例对象。
实验代码5:
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class MultiClassLoaderAttack {
public static void main(String[] args) throws Exception {
/**
* 注意先把类路径下的single/Singleton1.class,single/Singleton5.class,single/Singleton6.class文件删除,否则会被优先加载
*/
ClassLoader loader1 = new URLClassLoader(new URL[]{new File("/home/dslztx/Desktop/ca/").toURI().toURL()});
ClassLoader loader2 = new URLClassLoader(new URL[]{new File("/home/dslztx/Desktop/cb/").toURI().toURL()});
Class m01 = loader1.loadClass("single.Singleton1");
Class m02 = loader2.loadClass("single.Singleton1");
comp(m01, m02);
Class n01 = loader1.loadClass("single.Singleton5");
Class n02 = loader2.loadClass("single.Singleton5");
comp(n01, n02);
Class o01 = loader1.loadClass("single.Singleton6");
Class o02 = loader2.loadClass("single.Singleton6");
comp(o01, o02);
}
private static void comp(Class a, Class b) throws Exception {
// 以下结果都为false,因为同一个类被不同类加载器加载得到的Class对象不同
System.out.println(a == b);
Method methodA = a.getMethod("getInstance");
Method methodB = b.getMethod("getInstance");
System.out.println(methodA == methodB);
Object objA = methodA.invoke(null);
Object objB = methodB.invoke(null);
System.out.println(objA == objB);
// 以下结果都为true,因为在一个Class对象范畴中,存在“单例”语义,Class对象也只有一个
System.out.println(methodA.invoke(null) == objA);
System.out.println(methodB.invoke(null) == objB);
System.out.println(objA.getClass() == a);
System.out.println(objB.getClass() == b);
}
}
结论:
- 所有方案都不能避免该形式攻击
四、狭义单例和广义单例
上面所说的单例是“狭义单例”,我们日常所说的单例实际上是“广义单例”,两者的区别在于:
- 狭义单例,客观确保单例,防止恶意攻击
- 广义单例,主观确保单例,不考虑恶意攻击,比如“使用Spring框架中的Bean单例模式,实际上Bean对应类的构造方法是public的”
五、“单例”与“只执行一次”
“单例”与“只执行一次”的关系:实现“单例”必然涉及到“只执行一次”,但是“只执行一次”不必然是为了实现“单例”。比如如下代码:
public class KafkaProducerFactory {
private static void init() {
if (!init) {
synchronized (KafkaProducerFactory.class) {
if (!init) {
try {
Configuration configuration = ConfigLoadAssist.propConfig(CONFIG_FILE);
// doSomething1()
// doSomething2()
// doSomething3()
} catch (Exception e) {
logger.error("", e);
throw new RuntimeException(e);
} finally {
init = true;
}
}
}
}
}
}
使用哪种“只执行一次”方案来实现“单例”或者用于其他用途,具体情况具体分析。
参考文献
[1]https://cloud.tencent.com/developer/article/1446979
[2]http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
[3]https://juejin.im/post/5b50b0dd6fb9a04f932ff53f
[4]http://www.hollischuang.com/archives/2498
[5]http://ifeve.com/from-singleton-happens-before/
[6]http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
[7]https://www.hollischuang.com/archives/1144
[8]https://www.hollischuang.com/archives/205
[9]https://juejin.im/post/6844903753540173831
[10]https://www.cnblogs.com/giserxiaoliang/p/4922879.html