購物網(wǎng)站建設(shè)流程百度app客服人工電話
目錄
[設(shè)計(jì)模式]
單例模式
1. 餓漢模式
2. 懶漢模式
3. 單例模式的線程安全問題
[設(shè)計(jì)模式]
設(shè)計(jì)模式是軟件工程中的一種常見做法, 它可以理解為"模板", 是針對(duì)一些常見的特定場(chǎng)景, 給出的一些比較好的固定的解決方案.?
不同語言適用的設(shè)計(jì)模式是不一樣的. 這里我們接下來要談到的是java中典型的設(shè)計(jì)模式. 而且由于設(shè)計(jì)模式比較適合有一定編程經(jīng)驗(yàn)之后, 再去詳細(xì)學(xué)習(xí), 所以我們本篇文章就只討論幾個(gè)經(jīng)典的java設(shè)計(jì)模式
-
單例模式
在實(shí)際開發(fā)中, 某個(gè)進(jìn)程中, 我們不希望某個(gè)類有多個(gè)實(shí)例對(duì)象, 希望它有且僅有一個(gè)實(shí)例對(duì)象而且不能再創(chuàng)建出來. --> 這個(gè)時(shí)候我們就可以使用單例模式這樣的設(shè)計(jì)模式. 單例模式有兩種寫法, 一種叫餓漢模式, 一種叫懶漢模式. 下面我們就詳細(xì)討論一下這兩種單例模式的寫法.
1. 餓漢模式
"餓"的意思是"迫切的", 放到代碼中意思就是需要我們?cè)陬惐患虞d的時(shí)候就創(chuàng)建出這個(gè)單例的實(shí)例.?
class Singleton {private static Singleton instance = new Singleton();public static Singleton getInstance() { //獲取Singleton的實(shí)例對(duì)象, 但是每次獲取的都是相同的對(duì)象instance.return instance;}private Singleton() {} //單例模式中最關(guān)鍵的部分: 將構(gòu)造方法設(shè)置為私有. 防止在類外再創(chuàng)建出其他實(shí)例對(duì)象.
}
public class Demo24 {public static void main(String[] args) {Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();//兩次獲取到的對(duì)象應(yīng)該是同一個(gè)對(duì)象. 我們可以在下面驗(yàn)證一下 (== 比較的是兩個(gè)對(duì)象在內(nèi)存中的地址,也就是它們是否指向同一個(gè)實(shí)例對(duì)象)System.out.println(s1 == s2);}
}
?我們可以看到, 餓漢模式中,
(1) static修飾instance, 說明這里的instance是類成員(一個(gè)類只有一份, 隨著類的加載而創(chuàng)建出來)
(2) static修飾的類方法 getInstance() 每次返回的都是同一個(gè)對(duì)象 instance.
(3) 將SIngleton類的構(gòu)造方法設(shè)置為私有, 這就保證了在類外無法通過構(gòu)造方法再創(chuàng)建出新的對(duì)象.
我們通過在main方法中創(chuàng)建兩個(gè)引用s1和s2, 看到s1和s2指向的是同一個(gè)對(duì)象. 這也就代表了Singleton這個(gè)類只有一個(gè)實(shí)例.?
[注]: 上述單例模式只能避免程序員失誤, 調(diào)用了Singleton的構(gòu)造方法創(chuàng)建新對(duì)象;? 而無法避免程序員故意破壞單例模式(比如, 我們可以通過反射的方式拿到構(gòu)造方法).
2. 懶漢模式
我們先通過一個(gè)形象的例子來理解餓漢和懶漢的區(qū)別: 比如我們現(xiàn)在有一個(gè)編輯器, 要打開一個(gè)非常大的文本文檔. (1) 餓漢: 一啟動(dòng), 就把所有的文本內(nèi)容全都讀取到內(nèi)存中, 然后顯示到界面. (2) 懶漢: 先只加載出一部分?jǐn)?shù)據(jù), 隨著用戶的翻頁操作, 再按需加載剩下的內(nèi)容.? 根據(jù)上述表述, 我們可以確定, 懶漢模式一定比餓漢模式加載出來的速度更快, 用戶的體驗(yàn)也就會(huì)更好.?所以, 我們?nèi)粘i_發(fā)中, 很多地方都青睞于使用懶漢模式.
class SingletonLazy {private static SingletonLazy instance = null;public static SingletonLazy getInstance() {if (instance == null) {instance = new SingletonLazy();}return instance;// 如果instance為空,則創(chuàng)建一個(gè)實(shí)例對(duì)象; 如果instance不為空,則直接返回instance}private SingletonLazy() {}
}public class Demo25 {public static void main(String[] args) {SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();System.out.println(s1 == s2);}
}
如上述代碼, 當(dāng)我們首次調(diào)用getInstance時(shí), 由于此時(shí)對(duì)象還沒有創(chuàng)建, instance這個(gè)引用為空, 所以就會(huì)進(jìn)入if分支, 創(chuàng)建出SingletonLazy對(duì)象. 后續(xù)如果再重復(fù)調(diào)用getInstance, 結(jié)果都不會(huì)再創(chuàng)建新的實(shí)例, 而是直接返回instancomen
?我們還是通過在main方法中創(chuàng)建兩個(gè)引用s1和s2, 看到s1和s2指向的是同一個(gè)對(duì)象. 這也就代表了SingletonLazy這個(gè)類只有一個(gè)實(shí)例.
3. 單例模式的線程安全問題
知道了單例模式的兩種寫法之后, 我們現(xiàn)在要判斷: 這兩種寫法是否存在線程安全問題呢? (在多線程環(huán)境下, 多個(gè)線程調(diào)用getInstance, 是否會(huì)出現(xiàn)問題?)
(1) 餓漢模式:
我們可以看到, 餓漢模式的getInstance方法只涉及讀操作, 并沒有涉及任何寫操作. 而我們?cè)诙鄠€(gè)線程同時(shí)修改同一個(gè)變量時(shí), 才容易出現(xiàn)線程安全問題. 所以餓漢模式是線程安全的.
(2) 懶漢模式:
- 原子操作問題
像這種? 先條件判定, 再修改 的操作, 其實(shí)是典型的線程不安全代碼.?
比如, 我們現(xiàn)在有兩個(gè)線程同時(shí)調(diào)用getInstance方法. 線程t1先執(zhí)行if判斷, 判斷出instance為空. 此時(shí)t2線程插入進(jìn)來了, 執(zhí)行t2線程的if判斷, 那么t2的判斷結(jié)果同樣是空, t2就會(huì)執(zhí)行new SingletonLazy() 創(chuàng)建出一個(gè)新的對(duì)象. 而再切換回t1線程, 由于t1對(duì)instance的判斷也為空, 所以, t1也會(huì)執(zhí)行new SingletonLazy() 創(chuàng)建出一個(gè)新的對(duì)象. 那么這樣的話, Singletonlazy類就被實(shí)例化了兩次. 而單例模式要求類只能被實(shí)例化一次.
([注]: 雖然說后面創(chuàng)建的實(shí)例覆蓋了前面創(chuàng)建的實(shí)例, 前面創(chuàng)建的實(shí)例沒有引用變量引用的話很塊回被銷毀回收, 但是創(chuàng)建實(shí)例對(duì)象這個(gè)過程本身的開銷就很大(比如有的類一個(gè)實(shí)例就要100個(gè)G), 所以我們?nèi)匀徽J(rèn)為這個(gè)代碼是有bug的)
所以, 為了解決上述線程不安全問題, 我們就需要進(jìn)行"加鎖"操作. 將條件判斷和創(chuàng)建操作作為一個(gè)整體加上鎖, 這樣一來, if判斷和new創(chuàng)建操作這個(gè)整體就成了一個(gè)"原子"操作.? 這就保證了某個(gè)線程在順序執(zhí)行這兩個(gè)操作的時(shí)候不會(huì)有別的線程插入進(jìn)來.
class SingletonLazy {private static SingletonLazy instance = null;public static Object locker = new Object(); public static SingletonLazy getInstance() {synchronized(locker) {if (instance == null) {instance = new SingletonLazy();}return instance;// 如果instance為空,則創(chuàng)建一個(gè)實(shí)例對(duì)象; 如果instance不為空,則直接返回instance}}private SingletonLazy() {}
}
試想一下, 上述代碼, 如果已經(jīng)完成了創(chuàng)建對(duì)象的操作之后, 后續(xù)如果再調(diào)用getInstance, 就再也不會(huì)進(jìn)入if分支中去了, 都是簡單的讀操作(return instance). 那么只有讀操作的話, 不加鎖也是線程安全的. 我們知道, 加鎖這個(gè)操作, 對(duì)程序性能的影響還是挺大的.? 所以, 我們只需要在第一次執(zhí)行這個(gè)方法的時(shí)候(沒有創(chuàng)建出對(duì)象的時(shí)候)加鎖即可, 其他時(shí)候再執(zhí)行這個(gè)方法, 都是線程安全的, 不需要加鎖.?
那么, 如何判斷當(dāng)前是不是第一次調(diào)用這個(gè)方法呢? --> 看是否已經(jīng)創(chuàng)建出了實(shí)例對(duì)象, 如果還沒有instance對(duì)象, 那就是第一次調(diào)用, 需要對(duì)里面的判斷-創(chuàng)建對(duì)象 操作 加鎖;? 如果已經(jīng)有了instance對(duì)象, 那就不是第一次調(diào)用, 就不需要加鎖, 直接返回instance.
依據(jù)上述思考, 我們對(duì)代碼做出如下修改:
public static SingletonLazy getInstance() {if (instance == null) { //外層if: 判斷是否應(yīng)該加鎖synchronized(locker) {if (instance == null) { //內(nèi)層if: 判斷是否要?jiǎng)?chuàng)建對(duì)象instance = new SingletonLazy();}// 如果instance為空,則創(chuàng)建一個(gè)實(shí)例對(duì)象; 如果instance不為空,則直接返回instance}}return instance;}
注意: 這里, 外層if和內(nèi)層if雖然條件恰好是一樣的, 但是作用是完全不同的. 外層的if作用是: 判斷是否要加鎖. 內(nèi)層if的作用是: 判斷是否要?jiǎng)?chuàng)建新的對(duì)象.
- 指令重排序問題
編譯器在執(zhí)行創(chuàng)建對(duì)象的代碼時(shí), 為了提高性能, 可能會(huì)進(jìn)行"指令重排序"操作.
instance = new SingletonLazy();
編譯器在執(zhí)行這個(gè)創(chuàng)建對(duì)象代碼的時(shí)候, 會(huì)經(jīng)過如下步驟: (1) 分配內(nèi)存空間.? (2) 執(zhí)行構(gòu)造方法.? (3) 將對(duì)象的內(nèi)存空間地址賦給引用變量.? ?正常來說, 是按照(1) -> (2) -> (3) 的順序執(zhí)行的. 但是編譯器為了優(yōu)化性能, 也可能按照(1) -> (3) -> (2) 的順序執(zhí)行.
public static SingletonLazy getInstance() {if (instance == null) { //外層if: 判斷是否應(yīng)該加鎖synchronized(locker) {if (instance == null) { //內(nèi)層if: 判斷是否要?jiǎng)?chuàng)建對(duì)象instance = new SingletonLazy();}// 如果instance為空,則創(chuàng)建一個(gè)實(shí)例對(duì)象; 如果instance不為空,則直接返回instance}}return instance;}
那么, 我們?cè)囅? 如果在執(zhí)行完(1) -> (3) 之后, 此時(shí)有別的線程切入, 執(zhí)行if (instance == null) 判定, 那么此時(shí)判定instance就不為空了, 因?yàn)檎Z句指向了內(nèi)存空間(即使這個(gè)內(nèi)存空間里什么都沒有). 判定完instance不為空之后, 就會(huì)直接return instance. 那么如果這個(gè)線程拿到instance之后, 如果再調(diào)用里面的某個(gè)方法. 那么此時(shí)就會(huì)出現(xiàn)錯(cuò)誤!!! (因?yàn)閕nstance指向的內(nèi)存空間是未初始化的).
那么如何解決這個(gè)情況呢? --> volatile. 我們可以在instance前面加上一個(gè)volatile修飾, 告訴系統(tǒng), instance這個(gè)引用是"易變的, 易失的". 那么此時(shí)系統(tǒng)就會(huì)放棄對(duì)new SingletonLazy() 這個(gè)創(chuàng)建對(duì)象操作的優(yōu)化, 按照(1) -> (2) -> (3) 的順序執(zhí)行創(chuàng)建對(duì)象操作.這樣的話, 就不會(huì)出現(xiàn)上述問題了~
加上volatile的代碼最終如下:
class SingletonLazy {private static volatile SingletonLazy instance = null;public static Object locker = new Object();public static SingletonLazy getInstance() {if (instance == null) { //外層if: 判斷是否應(yīng)該加鎖synchronized(locker) {if (instance == null) { //內(nèi)層if: 判斷是否要?jiǎng)?chuàng)建對(duì)象instance = new SingletonLazy();}// 如果instance為空,則創(chuàng)建一個(gè)實(shí)例對(duì)象; 如果instance不為空,則直接返回instance}}return instance;}private SingletonLazy() {}
}
那么這樣一個(gè)單例模式的代碼無論在執(zhí)行效率還是在線程安全上就都沒有任何問題了.
好了, 本篇文章就介紹到這里啦, 大家如果有疑問歡迎評(píng)論, 如果喜歡小編的文章, 記得點(diǎn)贊收藏~~