單例模式的實現(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)初始化了就直接返回這個實例。
再來說這個寫法的問題,老生常談的線程安全問題。這個寫法完全沒有考慮多線程的情況。
假設有線程1,線程2兩個線程。線程1執(zhí)行了a之后,判斷實例是為空的;之后切換線程2,線程2當然也會執(zhí)行a,并且由于此時實例還未被初始化,所以,線程2會通過判斷,執(zhí)行b,初始化實例;切回線程1,線程1繼續(xù)執(zhí)行b,又將實例初始化了一次,此時對象實例已不唯一,破壞了單例模式。
2) 加鎖的方式
線程并發(fā)出現(xiàn)的問題大多可以用加鎖,也就是同步的方式解決,于是為getInstance方法加上synchronized關鍵字。
/**
* 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)生很大影響,我們應該讓在同步塊中的語句盡量少。現(xiàn)在來分析一下可以優(yōu)化的地方,這個方法也就3條語句,a、b、c。從第一種方式種的問題可以看出,主要成因與a和b有關,于是我們應該縮小同步塊范圍到這兩條語句。
/**
* 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條件去判斷實例是否為空。但是當然也必須保留原來在同步塊里面的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關鍵字去修飾。這是為了解決雙重檢測鎖存在的線程安全問題。
很多人覺得,這樣寫非常完美,怎么也看不出問題。但是如果不加volatile的確是有問題的,因為java虛擬機會進行指令重排。
volatile關鍵字
先來說說volatile關鍵字,它主要有兩個作用,他能夠保證變量的可見性,并且他能夠防止指令重排序,這里主要用到它的第二個作用。
指令重排
什么是指令重排序,結合以上代碼
singleton = new LazySingleton();
這只是一條語句,看上去只進行變量初始化一個簡單的操作,但是在java虛擬機層面,它是很復雜的,分為很多個操作,需要進行類加載檢查,分配內(nèi)存,初始化對象頭信息等等。不過解釋這里的問題,只需要將其大致分為三個部分:
(1)分配內(nèi)存
(2)調(diào)用構造函數(shù)
(3)賦值
這只是我們所覺得的正常的指令順序,但是java虛擬機在編譯時這些指令很可能變成:
(1)分配內(nèi)存
(2)賦值
(3)調(diào)用構造函數(shù)
因為對于單線程來說,這兩個指令順序并沒有什么區(qū)別,因為賦值和調(diào)用構造函數(shù)是沒有先后關系的,我可以先將對象內(nèi)存地址賦值給引用然后再去調(diào)用構造函數(shù)初始化對象的屬性,這樣得到的結果是一樣的。而且單線程的所有語句都是串行的,也就是順序執(zhí)行的,能夠保證在下一條語句執(zhí)行的時候,這三個指令都已執(zhí)行完成。
不過一旦到了多線程的環(huán)境中,就存在潛在問題,現(xiàn)在回到代碼。
在指令重排之后,當線程1執(zhí)行了(1)和(2)還未執(zhí)行(3)的時候,就切到線程2執(zhí)行,此時線程2在第一個if判斷時,對象雖然還不完整,但已經(jīng)不為空了,所以線程2會跳到return語句,直接返回一個不完整的對象,這樣只要線程1還沒有執(zhí)行完初始化操作中的第三條指令,你的程序就會繼續(xù)使用一個不完整的對象,這樣產(chǎn)生的后果肯定是不堪設想的。
而volatile關鍵字會杜絕關于被修飾變量的指令重排的發(fā)生,也就是說始終保持正常的指令順序,這樣保證只要語句singleton = new LazySingleton()沒有執(zhí)行完,singleton變量永遠為空。
在加上了volatile關鍵字后,DCL就能正常工作。
4)ThreadLocal方式
除了加volatile關鍵字外,要解決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變量,就是只屬于當前線程的局部變量,換句話說就是每個線程都會持有一個該threadlocal變量的副本,當不同的線程訪問同一個ThreadLocal變量,得到的值可能是不同的,它有兩個重要的方法,一個是get,一個是set,對應讀和寫。
怎么用它來解決DCL的問題呢,思路是這樣的:
在volatile方式中,已經(jīng)分析了問題所在,就是第一層if條件判斷時可能會出現(xiàn)不完整同時又不為空的對象實例,于是,將這里的判斷條件替換為只屬于當前線程的局部變量,因為這個局部變量一開始是為空的,所以無論線程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來管理的這些變量,鍵為線程對象,值為該線程對應的局部變量副本的值。
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();
運行結果:
可以看到只有外部類被加載了,內(nèi)部類并沒有被加載
加載內(nèi)部類
Out.loadIn();
結果:
結果證明了之前的結論。
其中需要注意的有兩點:
(1)這里的內(nèi)部類必須是靜態(tài)內(nèi)部類,原因很簡單,這里的單例模式獲取對象實例需要用到內(nèi)部類,而非靜態(tài)內(nèi)部類同非靜態(tài)變量一樣是對象級的,必須先有對象實例才能訪問,這樣就產(chǎn)生了矛盾。
并且只有靜態(tài)內(nèi)部類才能夠持有靜態(tài)變量和方法。至于為什么,目前我還沒找出好的解釋,所以就把它當成一個語法規(guī)定吧。
(2)內(nèi)部類的訪問修飾符應該為private或者protected,因為靜態(tài)內(nèi)部類在其他類中是可以被訪問的,這個雖然不影響單例模式,但是類應該盡量將具體的實現(xiàn)屏蔽起來,這樣外部就不會知道這個單例類的實現(xiàn)是采用的內(nèi)部類的方式了。(個人看法)
小結
對于單例模式,為了提高性能而通常選擇懶漢式的實現(xiàn),但是又帶來了許多線程安全問題,能解決這些問題的有3種實現(xiàn),帶volatile的DCL、用threadlocal的DCL、靜態(tài)內(nèi)部類。其中最簡單的是靜態(tài)內(nèi)部類的方式,也最容易理解。但是另外兩個方式對于多線程的學習和理解來講也是很重要的。
單例注冊表
上面提到的單例模式雖好,但是都有一點瑕疵,就是不能重用。如果我要將一個類變成單例的,我必須要在這個類上寫上面提到的那些代碼,過段時間,另一個類也需要寫成單例的,我又要寫上這些代碼。
而單例注冊表就是一種可以獲得任何類的唯一實例的一個表。只要是同一個單例注冊表獲取到的同一個類的實例,總是相同的。
先看看使用起來是怎么樣的,隨便建一個類用來測試:
/**
* 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());
看看結果:
兩個對象是一樣的。
其實這個單例注冊表的實現(xiàn)很簡單,就是用一個hashmap來維護單例對象。
代碼一看便知:
/**
* Created by liuruijie on 2017/2/13.
* 單例注冊表
*/
public class BeanFactory {
/**
* 這些是維護此注冊表的,
* 因為不是重點
* 所以采用了最簡單的方式
* 可以用其他方式
*/
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關鍵字的,為什么,因為在bean = Class.forName(className).newInstance();這句沒執(zhí)行完的時候,cacheMap中不可能有不完整的對象,只有在后面的cacheMap.put(className, bean);執(zhí)行之后,cacheMap中才會有對應的對象并且肯定是完整的。所以這里不需要加volatile。
可能會有人覺得,這些類名和方法名很熟悉,讓人聯(lián)想到spring框架,我想說的是spring就是采用這種方式來維護bean的單例性的。當然,要做好這樣一個類,上面那些肯定是不夠的。
看看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這段代碼涉及到的東西很多,而且將許多語句封裝成了方法,不過不用仔細看,從這幾句就知道單例注冊表在其中是有應用的。
最后的總結
本篇文章是我學習單例模式的筆記整理出來的,從單例模式的實現(xiàn)到單例模式的應用都有所涉及,其中還有許多地方可以深究,比如,延遲加載的各種并發(fā)問題,volatile關鍵字所涉及到的java的內(nèi)存模型,還有spring的單例模式具體實現(xiàn)等等。