單例模式伏嗜,是特別常見的一種設(shè)計(jì)模式粒蜈,因此我們有必要對(duì)它的概念和幾種常見的寫法非常了解,而且這也是面試中常問的知識(shí)點(diǎn)罐脊。
所謂單例模式定嗓,就是所有的請(qǐng)求都用一個(gè)對(duì)象來處理,如我們常用的Spring默認(rèn)就是單例的爹殊,而多例模式是每一次請(qǐng)求都創(chuàng)建一個(gè)新的對(duì)象來處理蜕乡,如structs2中的action。
使用單例模式梗夸,可以確保一個(gè)類只有一個(gè)實(shí)例层玲,并且易于外部訪問,還可以節(jié)省系統(tǒng)資源反症。如果在系統(tǒng)中辛块,希望某個(gè)類的對(duì)象只存在一個(gè),就可以使用單例模式铅碍。
那怎么確保一個(gè)類只有一個(gè)實(shí)例呢琢融?
我們知道老充,通常我們會(huì)通過new關(guān)鍵字來創(chuàng)建一個(gè)新的對(duì)象女坑。這個(gè)時(shí)候類的構(gòu)造函數(shù)是public公有的,你可以隨意創(chuàng)建多個(gè)類的實(shí)例憨愉。所以,首先我們需要把構(gòu)造函數(shù)改為private私有的卿捎,這樣就不能隨意new對(duì)象了配紫,也就控制了多個(gè)實(shí)例的隨意創(chuàng)建。
然后午阵,定義一個(gè)私有的靜態(tài)屬性躺孝,來代表類的實(shí)例,它只能類內(nèi)部訪問底桂,不允許外部直接訪問植袍。
最后,通過一個(gè)靜態(tài)的公有方法籽懦,把這個(gè)私有靜態(tài)屬性返回出去于个,這就為系統(tǒng)創(chuàng)建了一個(gè)全局唯一的訪問點(diǎn)。
以上猫十,就是單例模式的三個(gè)要素览濒。總結(jié)為:
- 私有構(gòu)造方法
- 指向自己實(shí)例的私有靜態(tài)變量
- 對(duì)外的靜態(tài)公共訪問方法
單例模式分為餓漢式和懶漢式拖云。它們的主要區(qū)別就是,實(shí)例化對(duì)象的時(shí)機(jī)不同应又。餓漢式宙项,是在類加載時(shí)就會(huì)實(shí)例化一個(gè)對(duì)象。懶漢式株扛,則是在真正使用的時(shí)候才會(huì)實(shí)例化對(duì)象尤筐。
餓漢式單例代碼實(shí)現(xiàn):
public class Singleton {
// 餓漢式單例,直接創(chuàng)建一個(gè)私有的靜態(tài)實(shí)例
private static Singleton singleton = new Singleton();
//私有構(gòu)造方法
private Singleton(){
}
//提供一個(gè)對(duì)外的靜態(tài)公有方法
public static Singleton getInstance(){
return singleton;
}
}
懶漢式單例代碼實(shí)現(xiàn)
public class Singleton {
// 懶漢式單例洞就,類加載時(shí)先不創(chuàng)建實(shí)例
private static Singleton singleton = null;
//私有構(gòu)造方法
private Singleton(){
}
//真正使用時(shí)才創(chuàng)建類的實(shí)例
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
稍有經(jīng)驗(yàn)的程序員就發(fā)現(xiàn)了盆繁,以上懶漢式單例的實(shí)現(xiàn)方式,在單線程下是沒有問題的旬蟋。但是油昂,如果在多線程中使用,就會(huì)發(fā)現(xiàn)它們返回的實(shí)例有可能不是同一個(gè)倾贰。我們可以通過代碼來驗(yàn)證一下冕碟。創(chuàng)建十個(gè)線程,分別啟動(dòng)匆浙,線程內(nèi)去獲得類的實(shí)例安寺,把實(shí)例的 hashcode 打印出來,只要相同則認(rèn)為是同一個(gè)實(shí)例首尼;若不同挑庶,則說明創(chuàng)建了多個(gè)實(shí)例言秸。
public class TestSingleton {
public static void main(String[] args) {
for (int i = 0; i < 10 ; i++) {
new MyThread().start();
}
}
}
class MyThread extends Thread {
@Override
public void run() {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());
}
}
/**
運(yùn)行多次,就會(huì)發(fā)現(xiàn)迎捺,hashcode會(huì)出現(xiàn)不同值
668770925
668770925
649030577
668770925
668770925
668770925
668770925
668770925
668770925
668770925
*/
所以井仰,以上懶漢式的實(shí)現(xiàn)方式是線程不安全的。那餓漢式呢破加?你可以手動(dòng)測(cè)試一下俱恶,會(huì)發(fā)現(xiàn)不管運(yùn)行多少次,返回的hashcode都是相同的范舀。因此合是,認(rèn)為餓漢式單例是線程安全的。
那為什么餓漢式就是線程安全的呢锭环?這是因?yàn)榇先I漢式單例在類加載時(shí),就創(chuàng)建了類的實(shí)例辅辩,也就是說在線程去訪問單例對(duì)象之前就已經(jīng)創(chuàng)建好實(shí)例了难礼。而一個(gè)類在整個(gè)生命周期中只會(huì)被加載一次。因此玫锋,也就可以保證實(shí)例只有一個(gè)蛾茉。所以說,餓漢式單例天生就是線程安全的撩鹿。(可以了解一下類加載機(jī)制)
既然懶漢式單例不是線程安全的谦炬,那么我們就需要去改造一下,讓它在多線程環(huán)境下也能正常工作节沦。以下介紹幾種常見的寫法:
- 使用synchronized方法
實(shí)現(xiàn)非常簡單键思,只需要在方法上加一個(gè)synchronized關(guān)鍵字即可
public class Singleton {
private static Singleton singleton = null;
private Singleton(){
}
//使用synchronized修飾方法,即可保證線程安全
public static synchronized Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
這種方式甫贯,雖然可以保證線程安全吼鳞,但是同步方法的作用域太大,鎖的粒度比較粗叫搁,因此赔桌,執(zhí)行效率就比較低。
- synchronized 同步塊
既然常熙,同步整個(gè)方法的作用域大纬乍,那我縮小范圍,在方法里邊裸卫,只同步創(chuàng)建實(shí)例的那一小部分代碼塊不就可以了嗎(因?yàn)榉椒ㄝ^簡單仿贬,所以鎖代碼塊和鎖方法沒什么明顯區(qū)別)。
public class Singleton {
private static Singleton singleton = null;
private Singleton(){
}
public static Singleton getInstance(){
//synchronized只修飾方法內(nèi)部的部分代碼塊
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
}
這種方法墓贿,本質(zhì)上和第一種沒什么區(qū)別茧泪,因此蜓氨,效率提升不大,可以忽略不計(jì)队伟。
- 雙重檢測(cè)(double check)
可以看到穴吹,以上的第二種方法只要調(diào)用getInstance方法,就會(huì)走到同步代碼塊里嗜侮。因此港令,會(huì)對(duì)效率產(chǎn)生影響。其實(shí)锈颗,我們完全可以先判斷實(shí)例是否已經(jīng)存在顷霹。若已經(jīng)存在,則說明已經(jīng)創(chuàng)建好實(shí)例了击吱,也就不需要走同步代碼塊了淋淀;若不存在即為空,才進(jìn)入同步代碼塊覆醇,這樣可以提高執(zhí)行效率朵纷。因此,就有以下雙重檢測(cè)了:
public class Singleton {
//注意永脓,此變量需要用volatile修飾以防止指令重排序
private static volatile Singleton singleton = null;
private Singleton(){
}
public static Singleton getInstance(){
//進(jìn)入方法內(nèi)袍辞,先判斷實(shí)例是否為空,以確定是否需要進(jìn)入同步代碼塊
if(singleton == null){
synchronized (Singleton.class){
//進(jìn)入同步代碼塊時(shí)也需要判斷實(shí)例是否為空
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
需要注意的一點(diǎn)是憨奸,此方式中革屠,靜態(tài)實(shí)例變量需要用volatile修飾。因?yàn)榕旁祝琻ew Singleton() 是一個(gè)非原子性操作,其流程為:
a.給 singleton 實(shí)例分配內(nèi)存空間
b.調(diào)用Singleton類的構(gòu)造函數(shù)創(chuàng)建實(shí)例
c.將 singleton 實(shí)例指向分配的內(nèi)存空間那婉,這時(shí)認(rèn)為singleton實(shí)例不為空
正常順序?yàn)?a->b->c板甘,但是,jvm為了優(yōu)化編譯程序详炬,有時(shí)候會(huì)進(jìn)行指令重排序盐类。就會(huì)出現(xiàn)執(zhí)行順序?yàn)?a->c->b。這在多線程中就會(huì)表現(xiàn)為呛谜,線程1執(zhí)行了new對(duì)象操作在跳,然后發(fā)生了指令重排序,會(huì)導(dǎo)致singleton實(shí)例已經(jīng)指向了分配的內(nèi)存空間(c)隐岛,但是實(shí)際上猫妙,實(shí)例還沒創(chuàng)建完成呢(b)。
這個(gè)時(shí)候聚凹,線程2就會(huì)認(rèn)為實(shí)例不為空割坠,判斷 if(singleton == null)為false齐帚,于是不走同步代碼塊,直接返回singleton實(shí)例(此時(shí)拿到的是未實(shí)例化的對(duì)象)彼哼,因此对妄,就會(huì)導(dǎo)致線程2的對(duì)象不可用而使用時(shí)報(bào)錯(cuò)。
4)使用靜態(tài)內(nèi)部類
思考一下敢朱,由于類加載是按需加載剪菱,并且只加載一次,所以能保證線程安全拴签,這也是為什么說餓漢式單例是天生線程安全的孝常。同樣的道理,我們是不是也可以通過定義一個(gè)靜態(tài)內(nèi)部類來保證類屬性只被加載一次呢篓吁。
public class Singleton {
private Singleton(){
}
//靜態(tài)內(nèi)部類
private static class Holder {
private static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
//調(diào)用內(nèi)部類的屬性茫因,獲取單例對(duì)象
return Holder.singleton;
}
}
而且,JVM在加載外部類的時(shí)候杖剪,不會(huì)加載靜態(tài)內(nèi)部類冻押,只有在內(nèi)部類的方法或?qū)傩裕ù颂幖粗竤ingleton實(shí)例)被調(diào)用時(shí)才會(huì)加載,因此不會(huì)造成空間的浪費(fèi)盛嘿。
5)使用枚舉類
因?yàn)槊杜e類是線程安全的洛巢,并且只會(huì)加載一次,所以利用這個(gè)特性次兆,可以通過枚舉類來實(shí)現(xiàn)單例稿茉。
public class Singleton {
private Singleton(){
}
//定義一個(gè)枚舉類
private enum SingletonEnum {
//創(chuàng)建一個(gè)枚舉實(shí)例
INSTANCE;
private Singleton singleton;
//在枚舉類的構(gòu)造方法內(nèi)實(shí)例化單例類
SingletonEnum(){
singleton = new Singleton();
}
private Singleton getInstance(){
return singleton;
}
}
public static Singleton getInstance(){
//獲取singleton實(shí)例
return SingletonEnum.INSTANCE.getInstance();
}
}