又到了一個老生常談的話題寸士,單例模式渤愁,可能在面試時我們也經(jīng)常會遇到牵祟,但是看似很簡單的問題,卻能看出一個人對單例理解的深度猴伶。要寫一個單例,首先需要讓構(gòu)造器私有塌西,還需要對外提供一個可以獲取單例的一個入口他挎,通常我們可能會這樣寫:
第一種:
public class SingleTon {
private static SingleTon instance = new SingleTon();
private SingleTon(){}
public static SingleTon get(){
return instance;
}
}
這種方式簡單直接,實(shí)例隨著類加載而加載捡需,很方便办桨,但是卻不友好,有時候我們雖然加載了類站辉,卻沒有使用該類實(shí)例的時候呢撞,會造成內(nèi)存的浪費(fèi),不能達(dá)到懶加載的能力饰剥。所以我們可以改進(jìn)成下面這樣:
第二種:
public class SingleTon {
private static SingleTon instance = null;
private SingleTon(){}
public static SingleTon get(){
if (instance == null){
instance =new SingleTon();
}
return instance;
}
}
這樣可以達(dá)到懶加載殊霞,需要的時候在初始化,但是如果在多線程的情況下是不完全的汰蓉,那我們會這樣寫:
public class SingleTon {
private static SingleTon instance = null;
private SingleTon(){}
public static synchronized SingleTon get(){
if (instance == null){
instance =new SingleTon();
}
return instance;
}
}
雖然這樣安全了绷蹲,但是鎖的粒度還是比較大,所以為了減小鎖的粒度我們還會這樣寫:
public class SingleTon {
private static SingleTon instance = null;
private SingleTon() {
}
public static SingleTon get() {
if (instance == null) {
synchronized (SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
}
return instance;
}
}
如果我們寫到這里就以為很滿足了顾孽,那么我只能說太天真了祝钢,看似一切完美,但是我們還是要問自己若厚,這樣就絕對的會線程安全嗎拦英?
要回答這個問題,這就不得不說一說對象的創(chuàng)建過程和java虛擬機(jī)的無序性测秸。首先在我們new對象的時候疤估,首先需要在方法區(qū)中去尋找該類的符號引用灾常,如果找不到,說明類還沒有被加載進(jìn)虛擬機(jī)做裙,所以需要通過類加載器先裝載該類岗憋,通過加載,驗(yàn)證锚贱,準(zhǔn)備仔戈,解析,初始化等操作拧廊,然后為對象在堆上開辟內(nèi)存空間(1)监徘,對象初始化操作(2),然后在將棧上的引用指向該對象內(nèi)存地址(3)吧碾。重點(diǎn)就在這凰盔,由于虛擬機(jī)的無序性,可能會造成執(zhí)行的順序并不是按照123進(jìn)行的倦春,也可能是按照132的執(zhí)行順序户敬,結(jié)果就是引用先指向?qū)ο蟮刂罚缓髮ο笤谶M(jìn)行初始化等操作睁本,這是由于線程的可見性造成的尿庐,所以為了保證變量instance的線程之間的可見性,我們需要將instance變量進(jìn)行volatile修飾來解決instance的可見性問題呢堰。(關(guān)于java虛擬機(jī)的無序性和volatile的內(nèi)存語義抄瑟,涉及到了java內(nèi)存模型的層面,這里暫時不過多分析枉疼,后面會單獨(dú)進(jìn)行講解)皮假。
所以正確的寫法是這樣:
public class SingleTon {
private static volatile SingleTon instance = null;
private SingleTon() {
}
public static SingleTon get() {
if (instance == null) {
synchronized (SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
}
return instance;
}
}
我們還可以改成成一個類,專門生成單例:
public abstract class Singleton<T> {
private volatile T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
那么到此我們就可以滿足了嗎骂维?當(dāng)然不能惹资。
這里不可避免的需要對volatile進(jìn)行解釋一下了,volatile在《深入理解java虛擬機(jī)》中有一下幾層含義:
1航闺,被volatile修飾的變量布轿,保證了該變量對其他線程的可見性,;
2来颤,禁止指令重排序汰扭,虛擬機(jī)會通過插入很多讀寫內(nèi)存屏障,來保證處理器不會亂序執(zhí)行福铅,但是也會造成編譯器不會對代碼進(jìn)行優(yōu)化(java內(nèi)存模型會最大限度的保證程序并行執(zhí)行)萝毛,對效率有一定影響。
那么我們在不使用volatile的前提下如何優(yōu)化呢滑黔,下面給出某大牛的寫法:
public class SingleTon {
private static SingleTon instance = null;
private SingleTon() {
}
public static SingleTon get() {
if (instance == null) {
synchronized (SingleTon.class) {
if (instance == null) {
SingleTon temp = null;
try {
temp = new SingleTon();
} catch (Exception e) {
}
if (temp != null)
instance = temp;
}
}
return instance;
}
}
看似無用的代碼卻大有用處笆包,try的存在虛擬機(jī)無法優(yōu)化temp是否為空环揽,instance在賦值之前保證了對象已經(jīng)初始化完成♀钟叮看到這里明顯感覺到水很深啊歉胶。
前面其實(shí)大概分為兩種,餓漢式和懶漢式巴粪,那有沒有既線程安全寫法簡單通今,又能懶加載呢?
第三種:
public class SingleTon {
private SingleTon() {
}
private static class SingleHolder {
private static SingleTon instance = new SingleTon();
}
public static SingleTon get() {
return SingleHolder.instance;
}
}
這里我們通過靜態(tài)內(nèi)部類來完成肛根,是不是很妙掸茅,我們無需枷鎖鄙麦,外部類的加載不會造成內(nèi)部類同時加載的,只有調(diào)用了get方法時才會加載內(nèi)部類筋夏,創(chuàng)建對象透葛,集前兩種方法的優(yōu)點(diǎn)于一身秽梅。
但是到這里我們又要分析了颇玷,以上寫法到底完全不锌云,如果通過反射或者反序列化還能保證是單例嗎?
當(dāng)然不可能褂乍,在反射面前持隧,一切都是小兒科了,這種寫法可阻止不了反射树叽,反序列化也不行舆蝴,你必須重寫readReslove方法谦絮,返回當(dāng)前實(shí)例题诵,不然就是多個實(shí)例了,
那到底有沒有絕對安全的單例啊层皱,我們是不是都快絕望了性锭,別急,放大招:
public enum SingleTon {
intstance;
}
是不是有點(diǎn)意外了叫胖,居然最簡單最安全的是枚舉草冈,至于枚舉是如何做到反射和反序列化時依然安全的可以看鏈接:
https://blog.csdn.net/gavin_dyson/article/details/70832185
好了,單例到此介紹完畢瓮增,看完這些你對單例模式真的了解了嗎怎棱?