單例模式的實現(xiàn)
單例模式的實現(xiàn)一般來說有2種方式:懶漢式(延遲加載)乳乌、餓漢式(非延遲加載)。
1. 餓漢式(非延遲加載)
/**
* Created by liuruijie on 2017/2/13.
* 餓漢式(非延遲加載)單例類
*/
public class HungrySingleton {
private static HungrySingleton hungSingleton = new HungrySingleton();
public static HungrySingleton getInstance() {
return hungSingleton;
}
private HungrySingleton(){}
}
以上代碼实牡,靜態(tài)變量在類被加載的時候初始化贱傀,之后就不會再執(zhí)行hungSingleton = new HungrySingleton();語句,所以保證了單例胎撤。
還有一種寫法晓殊,通過枚舉來實現(xiàn):
/**
* Created by liuruijie on 2017/2/13.
* 餓漢式(非延遲加載)單例類 -- 枚舉
*/
public enum LazySingleton {
SINGLETON_INSTANCE;
public static LazySingleton getInstance() {
return SINGLETON_INSTANCE;
}
}
就這么簡單。
餓漢式(非延遲加載)這種方式相對簡單伤提,也不會有什么安全問題巫俺,但是它的最大弊端顯而易見,就是唯一的實例在這個類被加載時就被創(chuàng)建了肿男,即還未使用實例介汹,資源就已經(jīng)被提前分配了却嗡。所以一般來說,為了提高性能嘹承,使用更多的還是懶漢式(延遲加載)窗价。
2. 懶漢式(延遲加載)
這個方式水就很深了,它的實現(xiàn)有好幾種赶撰,現(xiàn)在由淺入深一個一個看舌镶。
1) 最簡單的方式
/**
* Created by liuruijie on 2017/2/13.
* 懶漢式(延遲加載)單例類 -- 0
*/
public class LazySingleton {
private static LazySingleton singleton;
public static LazySingleton getInstance() {
//獲取實例之前檢查是否為空
if(singleton == null){ //a
//第一次獲取的時候總是為空的
//初始化實例
singleton = new LazySingleton(); //b
}
return singleton;
}
private LazySingleton(){}
}
先不說這個寫法的問題,可以看到這個寫法表明了懶漢式(延遲加載)的大概思路豪娜。在getInstance()方法種首先檢查唯一的實例是不是還沒被初始化餐胀,如果沒有就將其初始化后再返回,已經(jīng)初始化了就直接返回這個實例瘤载。
再來說這個寫法的問題否灾,老生常談的線程安全問題。這個寫法完全沒有考慮多線程的情況鸣奔。
假設(shè)有線程1墨技,線程2兩個線程。線程1執(zhí)行了a之后挎狸,判斷實例是為空的扣汪;之后切換線程2,線程2當(dāng)然也會執(zhí)行a锨匆,并且由于此時實例還未被初始化崭别,所以,線程2會通過判斷恐锣,執(zhí)行b茅主,初始化實例;切回線程1土榴,線程1繼續(xù)執(zhí)行b诀姚,又將實例初始化了一次,此時對象實例已不唯一玷禽,破壞了單例模式赫段。
2) 加鎖的方式
線程并發(fā)出現(xiàn)的問題大多可以用加鎖,也就是同步的方式解決矢赁,于是為getInstance方法加上synchronized關(guān)鍵字瑞佩。
/**
* Created by liuruijie on 2017/2/13.
* 懶漢式(延遲加載)單例類 -- 1
*/
public class LazySingleton {
private static LazySingleton singleton;
public static synchronized LazySingleton getInstance() {
//獲取實例之前檢查是否為空
if(singleton == null){ //a
//第一次獲取的時候總是為空的
//初始化實例
singleton = new LazySingleton(); //b
}
return singleton; //c
}
private LazySingleton(){}
}
此時問題是解決了,但是我們都知道加鎖同步會對性能產(chǎn)生很大影響坯台,我們應(yīng)該讓在同步塊中的語句盡量少。現(xiàn)在來分析一下可以優(yōu)化的地方瘫寝,這個方法也就3條語句蜒蕾,a稠炬、b、c咪啡。從第一種方式種的問題可以看出首启,主要成因與a和b有關(guān),于是我們應(yīng)該縮小同步塊范圍到這兩條語句撤摸。
/**
* Created by liuruijie on 2017/2/13.
* 懶漢式(延遲加載)單例類 -- 1
*/
public class LazySingleton {
private static LazySingleton singleton;
public static LazySingleton getInstance() {
synchronized(LazySingleton.class){
//獲取實例之前檢查是否為空
if(singleton == null){ //a
//第一次獲取的時候總是為空的
//初始化實例
singleton = new LazySingleton(); //b
}
}
return singleton; //c
}
private LazySingleton(){}
}
這樣看似很美妙毅桃,解決了線程安全問題,還優(yōu)化了性能准夷。但是钥飞,現(xiàn)在還沒有優(yōu)化徹底。想想看衫嵌,只有第一次读宙,對象實例還沒有初始化的時候,鎖才有意義楔绞,實例初始化之后结闸,不會再執(zhí)行語句b了,但是還是要經(jīng)過synchronized酒朵,這是無意義的桦锄,所以還能優(yōu)化。
3)雙重檢測鎖的方式
/**
* Created by liuruijie on 2017/2/13.
* 懶漢式(延遲加載)單例類 -- 2
*/
public class LazySingleton {
private static volatile LazySingleton singleton;
public static LazySingleton getInstance() {
if (singleton==null){ //a
synchronized(LazySingleton.class){ //b
//獲取實例之前檢查是否為空
if(singleton == null){ //c
//第一次獲取的時候總是為空的
//初始化實例
singleton = new LazySingleton(); //d
}
}
}
return singleton; //e
}
private LazySingleton(){}
}
什么叫雙重檢測鎖(Double Checked Locking,DCL)蔫耽,這么高大上的名字结耀,但它其實就是兩個if判斷語句加一個synchronized鎖 -_=。
由于我們希望在初始化實例之后不要經(jīng)過無意義的同步語句针肥。所以往外面再加一個沒有同步的if條件去判斷實例是否為空饼记。但是當(dāng)然也必須保留原來在同步塊里面的if語句,因為初衷就是對初始化對象時的判斷語句和賦值語句做同步處理慰枕。
這樣第一次具则,線程1執(zhí)行到c后,切換線程2具帮,線程2執(zhí)行到b會阻塞博肋,線程1執(zhí)行完同步塊中的d后,切換線程2蜂厅,線程2執(zhí)行c時匪凡,由于實例已經(jīng)初始化,所以不會去執(zhí)行d掘猿,而直接執(zhí)行e返回了病游。并且對象實例初始化之后,每次調(diào)用getInstance方法都會在a執(zhí)行后執(zhí)行調(diào)到e返回,不會再經(jīng)過synchronized了衬衬。
眼睛犀利的朋友可能看到了還有一個地方的不同买猖,那就是靜態(tài)變量singleton前面多了個volatile關(guān)鍵字去修飾。這是為了解決雙重檢測鎖存在的線程安全問題滋尉。
很多人覺得玉控,這樣寫非常完美,怎么也看不出問題狮惜。但是如果不加volatile的確是有問題的高诺,因為java虛擬機會進(jìn)行指令重排。
volatile關(guān)鍵字
先來說說volatile關(guān)鍵字碾篡,它主要有兩個作用虱而,他能夠保證變量的可見性,并且他能夠防止指令重排序耽梅,這里主要用到它的第二個作用薛窥。
指令重排
什么是指令重排序,結(jié)合以上代碼
singleton = new LazySingleton();
這只是一條語句眼姐,看上去只進(jìn)行變量初始化一個簡單的操作诅迷,但是在java虛擬機層面,它是很復(fù)雜的众旗,分為很多個操作罢杉,需要進(jìn)行類加載檢查,分配內(nèi)存贡歧,初始化對象頭信息等等滩租。不過解釋這里的問題,只需要將其大致分為三個部分:
(1)分配內(nèi)存
(2)調(diào)用構(gòu)造函數(shù)
(3)賦值
這只是我們所覺得的正常的指令順序利朵,但是java虛擬機在編譯時這些指令很可能變成:
(1)分配內(nèi)存
(2)賦值
(3)調(diào)用構(gòu)造函數(shù)
因為對于單線程來說律想,這兩個指令順序并沒有什么區(qū)別,因為賦值和調(diào)用構(gòu)造函數(shù)是沒有先后關(guān)系的绍弟,我可以先將對象內(nèi)存地址賦值給引用然后再去調(diào)用構(gòu)造函數(shù)初始化對象的屬性技即,這樣得到的結(jié)果是一樣的。而且單線程的所有語句都是串行的樟遣,也就是順序執(zhí)行的而叼,能夠保證在下一條語句執(zhí)行的時候,這三個指令都已執(zhí)行完成豹悬。
不過一旦到了多線程的環(huán)境中葵陵,就存在潛在問題,現(xiàn)在回到代碼瞻佛。
在指令重排之后脱篙,當(dāng)線程1執(zhí)行了(1)和(2)還未執(zhí)行(3)的時候,就切到線程2執(zhí)行,此時線程2在第一個if判斷時涡尘,對象雖然還不完整忍弛,但已經(jīng)不為空了,所以線程2會跳到return語句考抄,直接返回一個不完整的對象,這樣只要線程1還沒有執(zhí)行完初始化操作中的第三條指令蔗彤,你的程序就會繼續(xù)使用一個不完整的對象川梅,這樣產(chǎn)生的后果肯定是不堪設(shè)想的。
而volatile關(guān)鍵字會杜絕關(guān)于被修飾變量的指令重排的發(fā)生然遏,也就是說始終保持正常的指令順序贫途,這樣保證只要語句singleton = new LazySingleton()沒有執(zhí)行完,singleton變量永遠(yuǎn)為空待侵。
在加上了volatile關(guān)鍵字后丢早,DCL就能正常工作。
4)ThreadLocal方式
除了加volatile關(guān)鍵字外秧倾,要解決DCL的問題怨酝,還有一種方式,就是使用ThreadLocal那先。
/**
* Created by liuruijie on 2017/2/13.
* 懶漢式(延遲加載)單例類 -- 3
*/
public class LazySingleton {
private static ThreadLocal<LazySingleton> threadLocal = new ThreadLocal<>();
private static LazySingleton singleton;
public static LazySingleton getInstance() {
if (threadLocal.get()==null) { //a
synchronized (LazySingleton.class) { //b
//獲取實例之前檢查是否為空
if (singleton == null) { //c
//第一次獲取的時候總是為空的
//初始化實例
singleton = new LazySingleton(); //d
}
}
threadLocal.set(singleton); //e
}
return singleton; //f
}
private LazySingleton(){}
}
什么是ThreadLocal變量农猬,就是只屬于當(dāng)前線程的局部變量,換句話說就是每個線程都會持有一個該threadlocal變量的副本售淡,當(dāng)不同的線程訪問同一個ThreadLocal變量斤葱,得到的值可能是不同的,它有兩個重要的方法揖闸,一個是get揍堕,一個是set,對應(yīng)讀和寫汤纸。
怎么用它來解決DCL的問題呢衩茸,思路是這樣的:
在volatile方式中,已經(jīng)分析了問題所在蹲嚣,就是第一層if條件判斷時可能會出現(xiàn)不完整同時又不為空的對象實例递瑰,于是,將這里的判斷條件替換為只屬于當(dāng)前線程的局部變量隙畜,因為這個局部變量一開始是為空的抖部,所以無論線程1是否執(zhí)行完語句d,線程2议惰,線程3慎颗,的threadlocal變量都是為空的,第一個if判斷條件都會通過,接著就是同步塊了俯萎,等待線程1執(zhí)行完語句d傲宜,也就是對象實例初始化完成之后,第二層的if判斷條件不會滿足夫啊,接著各個線程分別執(zhí)行了threadloacal.set函卒,也就是語句e后,其threadlocal變量就不為空了撇眯,之后便不會通過第一層的if條件报嵌,跳到語句f返回實例。
threadlocal避開了DCL的問題熊榛,但是卻增大了內(nèi)存開銷锚国,因為threadlocal本質(zhì)上是用一個hashmap來管理的這些變量,鍵為線程對象玄坦,值為該線程對應(yīng)的局部變量副本的值血筑。
5)內(nèi)部類方式
除了使用DCL之外,延遲加載的單例模式還可以通過內(nèi)部類來實現(xiàn)煎楣。
/**
* Created by liuruijie on 2017/2/13.
* 懶漢式(延遲加載)單例類 -- 3
*/
public class LazySingleton {
public static LazySingleton getInstance() {
//直接返回內(nèi)部類中的實例對象
return LazyHolder.lazySingleton;
}
private LazySingleton(){}
//靜態(tài)內(nèi)部類
private static class LazyHolder{
//持有外部類的實例豺总,并初始化
private static LazySingleton lazySingleton = new LazySingleton();
}
}
主要思路是,內(nèi)部類持有外部類的靜態(tài)實例转质,并將其在內(nèi)部類被加載時就初始化园欣。然后在外部類中的getInstance方法中返回此實例。類似餓漢式休蟹,由于內(nèi)部類只會被加載一次沸枯,也就是只會執(zhí)行一次初始化語句,所以保證了實例的唯一赂弓。
而內(nèi)部類什么時候加載绑榴,其實所有的類都是在使用它的時候被加載的,包括內(nèi)部類盈魁。所以內(nèi)部類不會隨著外部類的加載而加載翔怎,只有在使用它的時候才會被加載。
使用一個類情況有哪些杨耙?
1.調(diào)用靜態(tài)變量
2.調(diào)用靜態(tài)方法
3.創(chuàng)建此類對象
做一個簡單的實驗就明白了赤套。
/**
* Created by liuruijie on 2017/2/14.
* 內(nèi)部類與外部類加載時機
*/
public class Out {
static {
System.out.println("外部類被加載");
}
public static void loadOut(){
//這個方法不使用內(nèi)部類
}
public static void loadIn(){
int a = In.num; //通過調(diào)用靜態(tài)變量來使用內(nèi)部類
}
private static class In{
static int num;
static {
System.out.println("內(nèi)部類被加載");
}
}
}
用到的是只要一加載類就會執(zhí)行的靜態(tài)代碼塊來驗證。
首先只加載外部類
Out.loadOut();
運行結(jié)果:
可以看到只有外部類被加載了珊膜,內(nèi)部類并沒有被加載
加載內(nèi)部類
Out.loadIn();
結(jié)果:
結(jié)果證明了之前的結(jié)論容握。
其中需要注意的有兩點:
(1)這里的內(nèi)部類必須是靜態(tài)內(nèi)部類,原因很簡單车柠,這里的單例模式獲取對象實例需要用到內(nèi)部類剔氏,而非靜態(tài)內(nèi)部類同非靜態(tài)變量一樣是對象級的塑猖,必須先有對象實例才能訪問,這樣就產(chǎn)生了矛盾谈跛。
并且只有靜態(tài)內(nèi)部類才能夠持有靜態(tài)變量和方法羊苟。至于為什么,目前我還沒找出好的解釋感憾,所以就把它當(dāng)成一個語法規(guī)定吧蜡励。
(2)內(nèi)部類的訪問修飾符應(yīng)該為private或者protected,因為靜態(tài)內(nèi)部類在其他類中是可以被訪問的阻桅,這個雖然不影響單例模式巍虫,但是類應(yīng)該盡量將具體的實現(xiàn)屏蔽起來,這樣外部就不會知道這個單例類的實現(xiàn)是采用的內(nèi)部類的方式了鳍刷。(個人看法)
小結(jié)
對于單例模式,為了提高性能而通常選擇懶漢式的實現(xiàn)俯抖,但是又帶來了許多線程安全問題输瓜,能解決這些問題的有3種實現(xiàn),帶volatile的DCL芬萍、用threadlocal的DCL尤揣、靜態(tài)內(nèi)部類。其中最簡單的是靜態(tài)內(nèi)部類的方式柬祠,也最容易理解北戏。但是另外兩個方式對于多線程的學(xué)習(xí)和理解來講也是很重要的。
單例注冊表
上面提到的單例模式雖好漫蛔,但是都有一點瑕疵嗜愈,就是不能重用。如果我要將一個類變成單例的莽龟,我必須要在這個類上寫上面提到的那些代碼蠕嫁,過段時間,另一個類也需要寫成單例的毯盈,我又要寫上這些代碼剃毒。
而單例注冊表就是一種可以獲得任何類的唯一實例的一個表。只要是同一個單例注冊表獲取到的同一個類的實例搂赋,總是相同的赘阀。
先看看使用起來是怎么樣的,隨便建一個類用來測試:
/**
* Created by liuruijie on 2017/2/13.
* 隨便建的一個類
*/
public class Student {
//假裝有很多屬性和方法
}
獲取這個類的單例:
//這是一個單例注冊表
BeanFactory beanFactory = BeanFactory.getInstance();
//獲取實例1
Student student1 = (Student) beanFactory
.getBean(Student.class.getName());
//獲取實例2
Student student2 = (Student) beanFactory
.getBean(Student.class.getName());
//比較獲取到的兩個實例
System.out.println(student1.hashCode());
System.out.println(student2.hashCode());
看看結(jié)果:
兩個對象是一樣的脑奠。
其實這個單例注冊表的實現(xiàn)很簡單基公,就是用一個hashmap來維護(hù)單例對象。
代碼一看便知:
/**
* Created by liuruijie on 2017/2/13.
* 單例注冊表
*/
public class BeanFactory {
/**
* 這些是維護(hù)此注冊表的捺信,
* 因為不是重點
* 所以采用了最簡單的方式
* 可以用其他方式
*/
private static BeanFactory beanFactory = new BeanFactory();
private BeanFactory(){
}
public static BeanFactory getInstance() {
return beanFactory;
}
//緩存單例對象的hash表
private final HashMap<String, Object> cacheMap = new HashMap<>();
//通過類名獲取其單例對象
public Object getBean(String className) {
Object bean = cacheMap.get(className);
//使用雙重檢測鎖來實現(xiàn)單例
if (bean == null) {
synchronized (this.cacheMap) {
//第二次檢測
bean = cacheMap.get(className);
if (bean == null) {
try {
bean = Class.forName(className).newInstance();
} catch (InstantiationException e) {
System.err.println("could not instance an object of type:" + className);
e.printStackTrace();
} catch (IllegalAccessException e) {
System.err.println("could not access class " + className);
e.printStackTrace();
} catch (ClassNotFoundException e) {
System.err.println("could not find class " + className);
e.printStackTrace();
}
}
cacheMap.put(className, bean);
}
}
return bean;
}
}
注意這里的cacheMap是沒有加volatile關(guān)鍵字的酌媒,為什么欠痴,因為在bean = Class.forName(className).newInstance();這句沒執(zhí)行完的時候,cacheMap中不可能有不完整的對象秒咨,只有在后面的cacheMap.put(className, bean);執(zhí)行之后喇辽,cacheMap中才會有對應(yīng)的對象并且肯定是完整的。所以這里不需要加volatile雨席。
可能會有人覺得菩咨,這些類名和方法名很熟悉,讓人聯(lián)想到spring框架陡厘,我想說的是spring就是采用這種方式來維護(hù)bean的單例性的抽米。當(dāng)然,要做好這樣一個類糙置,上面那些肯定是不夠的云茸。
看看spring這段源碼:
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
···
protected <T> T doGetBean(String name, Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException {
final String beanName = this.transformedBeanName(name);
Object sharedInstance = this.getSingleton(beanName);
Object bean;
if(sharedInstance != null && args == null) {
···
bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, (RootBeanDefinition)null);
}else{
final RootBeanDefinition ex1 = this.getMergedLocalBeanDefinition(beanName);
···
if(ex1.isSingleton()) {
sharedInstance = this.getSingleton(beanName, new ObjectFactory() {
public Object getObject() throws BeansException {
try {
return AbstractBeanFactory.this.createBean(beanName, ex1, args);
} catch (BeansException var2) {
AbstractBeanFactory.this.destroySingleton(beanName);
throw var2;
}
}
});
bean = this.getObjectForBeanInstance(sharedInstanc, name, beanName, ex1);
}
···
return bean;
}
}
spring這段代碼涉及到的東西很多,而且將許多語句封裝成了方法谤饭,不過不用仔細(xì)看标捺,從這幾句就知道單例注冊表在其中是有應(yīng)用的。
最后的總結(jié)
本篇文章是我學(xué)習(xí)單例模式的筆記整理出來的揉抵,從單例模式的實現(xiàn)到單例模式的應(yīng)用都有所涉及亡容,其中還有許多地方可以深究,比如冤今,延遲加載的各種并發(fā)問題闺兢,volatile關(guān)鍵字所涉及到的java的內(nèi)存模型,還有spring的單例模式具體實現(xiàn)等等戏罢。