不一樣的單例模式
提起單例模式,大家基本上都不是很陌生蜡坊,它的主要作用是保證在Java的整個(gè)項(xiàng)目中只有一個(gè)對(duì)象的存在惕医,而大家在搜單利模式的時(shí)候基本上也會(huì)搜出各種各樣的寫(xiě)法,比如餓漢式算色,懶漢式抬伺,雙重校驗(yàn)鎖,靜態(tài)代碼塊灾梦,靜態(tài)內(nèi)部類峡钓,枚舉等等的寫(xiě)法,基本上算是老生常談的東西了若河,但是無(wú)論是面試還是日常開(kāi)發(fā)中能岩,單例模式還是挺常用到的,這里介紹一種不一樣寫(xiě)法的單例模式萧福,能幫助大家稍微提升一點(diǎn)逼格~
1. 目標(biāo)
作為單例模式拉鹃,我們有兩個(gè)基本目標(biāo):
- 全局唯一
- 線程安全
全局唯一和線程安全就不說(shuō)了,基本上所有的單例都能滿足這兩點(diǎn)鲫忍,那么最好我們的單例可以支持懶加載膏燕,同時(shí)在保證線程安全的情況下還能夠高效一些
2. 代碼
首先我們來(lái)看一下代碼:
import java.util.concurrent.atomic.AtomicReference;
/**
* If there are no bugs, it was created by Chen FengYao on 18-7-16;
* Otherwise, I don't know who created it either
*/
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
public static Singleton getInstance() {
for (; ; ) {
Singleton current = INSTANCE.get();
if (current != null) {
return current;
}
current = new Singleton();
if (INSTANCE.compareAndSet(null, current)) {
return current;
}
}
}
private Singleton() {
}
}
我們看到在這種單例的寫(xiě)法中,Singleton的實(shí)例實(shí)在調(diào)用getInstance方法的時(shí)候才被創(chuàng)建出來(lái)的悟民,也就是支持懶加載坝辫,而整段代碼也并沒(méi)有使用任何的線程鎖,而這個(gè)單例得以實(shí)現(xiàn)的核心是AtomicReference這個(gè)類
AtomicReference
根據(jù)API的描述射亏,它是一個(gè)可以保證對(duì)象更新原子性的一個(gè)類近忙,原子意味著多個(gè)線程試圖改變同一個(gè)AtomicReference將不會(huì)引發(fā)線程安全的問(wèn)題
AtomicReference有一個(gè)非常常用的方法:compareAndSet,這個(gè)方法接受兩個(gè)參數(shù)智润,第一個(gè)參數(shù)為期望值及舍,第二個(gè)參數(shù)為你想要設(shè)定的值,這個(gè)方法的含義是窟绷,將AtomicReference中的值更新為第二個(gè)參數(shù)所傳遞的值锯玛,當(dāng)當(dāng)前值為期望值的時(shí)候,如果更新成功钾麸,則返回true更振,否則返回false
單例說(shuō)明
那么接下來(lái)我們來(lái)看看這個(gè)單例的執(zhí)行流程
首先當(dāng)?shù)谝淮握{(diào)用getInstance的時(shí)候炕桨,INSTANCE中并沒(méi)有存儲(chǔ)任何的值,所以current為null肯腕,那么這時(shí)就會(huì)創(chuàng)建current對(duì)象献宫,并嘗試向INSTANCE中更新Singleton的值,只有當(dāng)INSTANCE中的值為null的時(shí)候才可能更新成功实撒,這就保證了在多線程環(huán)境中姊途,只能對(duì)INSTANCE中的值賦值一次,就保證了線程安全
3. 破壞單例
我們?cè)趯?xiě)單例的時(shí)候知态,總是不期望在工程中有多個(gè)實(shí)例的出現(xiàn)捷兰,于是我們將構(gòu)造方法私有化,并且提供了一個(gè)我們可以掌控的入口來(lái)創(chuàng)建出一個(gè)對(duì)象负敏,雖然如此贡茅,我們寫(xiě)的單例模式還是有被破壞的可能,所謂破壞單例其做,就是通過(guò)某種手段在整個(gè)工程中創(chuàng)建出多個(gè)實(shí)例顶考,總體來(lái)說(shuō),破會(huì)單例的方式有兩種:
- 通過(guò)反射
- 通過(guò)通過(guò)序列化
3.1 通過(guò)反射來(lái)破壞單例
我們知道妖泄,通過(guò)Java的反射技術(shù)驹沿,我們的代碼幾乎處于一種“為所欲為”的狀態(tài),雖然我們?cè)谧约旱膯卫愔袑?gòu)造方法私有化了蹈胡,但是可以通過(guò)反射輕松的創(chuàng)建出對(duì)象渊季,為了讓效果更加的明顯,首先在單例類中增加一個(gè)成員變量:
import java.util.concurrent.atomic.AtomicReference;
/**
* If there are no bugs, it was created by Chen FengYao on 18-7-16;
* Otherwise, I don't know who created it either
*/
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
// 增加的成員變量
private String name;
public static Singleton getInstance() {
for (; ; ) {
Singleton current = INSTANCE.get();
if (current != null) {
return current;
}
current = new Singleton();
if (INSTANCE.compareAndSet(null, current)) {
return current;
}
}
}
private Singleton() {
}
// setter/getter 方法
public String getName() {
return name;
}
public Singleton setName(String name) {
this.name = name;
return this;
}
}
然后我們通過(guò)getInstance方法獲得一個(gè)實(shí)例對(duì)象,在通過(guò)反射來(lái)獲取一個(gè)實(shí)例對(duì)象:
import java.lang.reflect.Constructor;
/**
* If there are no bugs, it was created by Chen FengYao on 18-7-15;
* Otherwise, I don't know who created it either
*/
public class Main {
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
singleton.setName("Tom");
Class<Singleton> singletonClazz = Singleton.class;
Constructor<Singleton> declaredConstructor = singletonClazz.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Singleton singleton1 = declaredConstructor.newInstance();
singleton1.setName("Jerry");
System.out.println(singleton.getName());
System.out.println(singleton1.getName());
}
}
運(yùn)行結(jié)果:
從運(yùn)行結(jié)果我們可以看出singleton和singleton1是兩個(gè)對(duì)象,就是通過(guò)反射我們調(diào)用了私有化的構(gòu)造方法,如果需要抵御這種攻擊,可以修改構(gòu)造器,通過(guò)獲取方法調(diào)用棧信息來(lái)判斷究竟是我們自己的getInstance方法調(diào)用的還是通過(guò)反射調(diào)用的,如果是通過(guò)反射調(diào)用的,那么我們就拋出一個(gè)運(yùn)行時(shí)異常,在Java中我們可以通過(guò)Throwable類來(lái)獲取方法調(diào)用堆棧信息,首先看看效果,在Singleton的構(gòu)造方法中添加代碼:
private Singleton() {
Throwable ex = new Throwable();
StackTraceElement[] stackElements = ex.getStackTrace();
if (stackElements != null) {
for (int i = 0; i < stackElements.length; i++) {
System.out.println(stackElements[i].getClassName());
System.out.println(stackElements[i].getFileName());
System.out.println(stackElements[i].getLineNumber());
System.out.println(stackElements[i].getMethodName());
System.out.println("-----------------------------------");
}
}
}
然后首先看一下通過(guò)正常的getInstance來(lái)獲取對(duì)象時(shí)的日志:
可以看到,在第二次循環(huán)中,發(fā)現(xiàn)類名為Singleton這個(gè)類,在看看通過(guò)反射調(diào)用的方法棧:
那現(xiàn)在一目了然了,可以看到如果是通過(guò)反射調(diào)用的,在方法的調(diào)用棧中是不會(huì)出現(xiàn)getInstance這個(gè)方法,或者Singleton這個(gè)類的其他信息的,那么我們可以通過(guò)去查詢方法調(diào)用棧來(lái)去判斷是否有人想要通過(guò)反射來(lái)破壞我們的單例,如果有,我們就拋出一個(gè)運(yùn)行時(shí)異常,改造后的代碼如下:
import java.util.concurrent.atomic.AtomicReference;
/**
* If there are no bugs, it was created by Chen FengYao on 18-7-16;
* Otherwise, I don't know who created it either
*/
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
// 增加的成員變量
private String name;
public static Singleton getInstance() {
for (; ; ) {
Singleton current = INSTANCE.get();
if (current != null) {
return current;
}
current = new Singleton();
if (INSTANCE.compareAndSet(null, current)) {
return current;
}
}
}
private Singleton() {
IllegalStateException illegalStateException = new IllegalStateException("不能調(diào)用構(gòu)造方法,請(qǐng)使用getInstance()來(lái)獲取實(shí)例");
StackTraceElement[] stackElements = illegalStateException.getStackTrace();
if (stackElements != null && !stackElements[1].getClassName().equals(getClass().getName())) {
throw illegalStateException;
}
}
// setter/getter 方法
public String getName() {
return name;
}
public Singleton setName(String name) {
this.name = name;
return this;
}
}
主要是改造了它的構(gòu)造方法,在這里選擇的是判斷調(diào)用棧的類名,即調(diào)用構(gòu)造方法的類一定是本類,否則就是通過(guò)非法途徑調(diào)用的,之所以選擇類名,因?yàn)轭惷彩强梢詣?dòng)態(tài)獲取的,這樣代碼一旦寫(xiě)完,后期無(wú)論是想改Singleton這個(gè)類名,還是想改getInstance這個(gè)方法名都是沒(méi)有問(wèn)題的,不需要再改構(gòu)造方法里面的代碼了,運(yùn)行一下看看效果:
public class Main {
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
singleton.setName("Tom");
System.out.println(singleton.getName());
System.out.println("+++++++++++++++");
Class<Singleton> singletonClazz = Singleton.class;
Constructor<Singleton> declaredConstructor = singletonClazz.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Singleton singleton1 = declaredConstructor.newInstance();
singleton1.setName("Jerry");
}
}
運(yùn)行結(jié)果:
可以看到使用getInstance方法調(diào)用就不會(huì)有問(wèn)題,而使用反射去調(diào)用構(gòu)造方法就會(huì)拋出異常,讓程序崩潰
3.2 通過(guò)序列化來(lái)破壞單例
如果一個(gè)單例類需要被序列化,那在反序列化的過(guò)程中是很有可能破壞單例的設(shè)計(jì)初衷的,因?yàn)榉葱蛄谢怯锌赡芾@過(guò)構(gòu)造方法的,首先讓Singleton 實(shí)現(xiàn)Serializable接口,然后編寫(xiě)測(cè)試代碼:
public class Main {
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
singleton.setName("Tom");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
oos.writeObject(singleton);
oos.close();
singleton.setName("Tom0");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton"));
Singleton singleton1 = (Singleton) ois.readObject();
ois.close();
singleton1.setName("Tom1");
System.out.println(singleton.getName());
System.out.println(singleton1.getName());
}
}
可以看到反序列化回來(lái)的并不再是原來(lái)的單例對(duì)象了,如果想要讓反序列話回來(lái)的還是單例對(duì)象,需要在單例類中添加readResolve方法,來(lái)自己實(shí)現(xiàn)反序列化的規(guī)則:
public class Singleton implements Serializable {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
// 增加的成員變量
private String name;
public static Singleton getInstance() {
for (; ; ) {
Singleton current = INSTANCE.get();
if (current != null) {
return current;
}
current = new Singleton();
if (INSTANCE.compareAndSet(null, current)) {
return current;
}
}
}
private Singleton() {
IllegalStateException illegalStateException = new IllegalStateException("不能調(diào)用構(gòu)造方法,請(qǐng)使用getInstance()來(lái)獲取實(shí)例");
StackTraceElement[] stackElements = illegalStateException.getStackTrace();
if (stackElements != null && !stackElements[1].getClassName().equals(getClass().getName())) {
throw illegalStateException;
}
}
// 用于反序列化
private Object readResolve(){
return getInstance();
}
// setter/getter 方法
public String getName() {
return name;
}
public Singleton setName(String name) {
this.name = name;
return this;
}
}
再次運(yùn)行:
他們就是同一個(gè)對(duì)象了