一、含义
“单例”的书面含义是:在一个Java进程中,某个给定类的实例只允许存在一个。
二、实现方案
实现“单例”方案须满足两个核心目标:
常见的单例实现方案有6种,具体可细分为3类:静态初始化锁机制,内置锁/高级锁,枚举。
接下来基于以下几个维度对上述6种单例实现方案进行评价:
- 实现可读性,用5分制来进行评价
- 性能
- 是采用“懒汉模式(在获取单例实例时才生成)”还是“饿汉模式(在获取单例实例前已生成)”
2.1、静态初始化锁机制
利用静态初始化锁机制来获得“确保单例”和“线程安全”。
1、方案1
1 2 3 4 5 6 7 8 9 10 11 12 13
| package single;
public class Singleton1 {
private static Singleton1 instance = new Singleton1();
private Singleton1() { }
public static Singleton1 getInstance() { return instance; } }
|
实现可读性 |
性能 |
懒汉模式/饿汉模式 |
5 |
好 |
饿汉模式 |
2、方案2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package single;
public class Singleton2 {
private static Singleton2 instance;
static { instance = new Singleton2(); }
private Singleton2() { }
public static Singleton2 getInstance() { return instance; } }
|
一个简单变种。
实现可读性 |
性能 |
懒汉模式/饿汉模式 |
5 |
好 |
饿汉模式 |
3、方案3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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”变量可能还未完成初始化,只不过是由于重排序先返回了变量引用而已。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| 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 + 构造方法
的形式创建新实例,因此方案6天然防御该形式攻击
- 方案1-5为防御
new + 构造方法
攻击,可将构造方法设为由private
修饰
3.2、克隆
类继承java.lang.Cloneable
接口,并覆盖实现clone()
方法,调用clone()
方法可以创建生成一个新的实例。
实验代码2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| enum F implements Cloneable { SINGLE;
char c;
double d;
F() { init(); }
public static F getInstance() { return SINGLE; }
private void init() { c = 'a'; d = 20D; }
}
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);
} }
|
结论:
- 枚举类不能覆盖实现
clone()
方法,因此方案6天然防御该形式攻击
- 方案1-5为防御
克隆
攻击,可禁止继承java.lang.Cloneable
接口,或者在继承的情况下,在覆盖实现的clone()
方法中返回已经生成的实例,而不是重新生成,类似如下代码
1 2 3 4
| @Override public Object clone() throws CloneNotSupportedException { return instance; }
|
3.3、反射
通过反射获取Constructor
对象,然后调用newInstance()
方法生成实例。
实验代码3:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| 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);
System.out.println(J.getInstance() == jConstructor.newInstance());
Constructor kConstructor = K.class.getDeclaredConstructor(); kConstructor.setAccessible(true);
System.out.println(K.getInstance() == kConstructor.newInstance());
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为防御
反射
攻击,对于“饿汉方案”,可在构造方法中增加“重复生成则抛出异常”的判断逻辑,类似如下代码;而对于“懒汉方案”,则无能为力
1 2 3
| if (instance != null) { throw new RuntimeException("the instance exists"); }
|
3.4、序列化-反序列化
序列化存在多种形式,这里讨论常见的JDK序列化和JSON序列化(以Fastjson序列化为例)。
实验代码4:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
| 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);
System.out.println(g == anotherG);
H h = H.getInstance();
H anotherH = (H) serializeAndDeserializeObjectJDK(h);
System.out.println(h == anotherH);
I i = I.getInstance();
I anotherI = (I) serializeAndDeserializeObjectJDK(i);
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);
System.out.println(g == anotherG);
H h = H.getInstance();
H anotherH = (H) serializeAndDeserializeObjectFastjson(h, H.class);
System.out.println(h == anotherH);
I i = I.getInstance();
I anotherI = (I) serializeAndDeserializeObjectFastjson(i, I.class);
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| 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 {
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 {
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);
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的”
五、“单例”与“只执行一次”
“单例”与“只执行一次”的关系:实现“单例”必然涉及到“只执行一次”,但是“只执行一次”不必然是为了实现“单例”。比如如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class KafkaProducerFactory { private static void init() { if (!init) { synchronized (KafkaProducerFactory.class) { if (!init) { try {
Configuration configuration = ConfigLoadAssist.propConfig(CONFIG_FILE);
} 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