多线程--单例模式
單例模式是23種設(shè)計(jì)模式中比較簡(jiǎn)單的模式之一,本博客較為詳細(xì)的梳理了該設(shè)計(jì)模式,并實(shí)現(xiàn)該模式。
問題由來 : 我們?yōu)槭裁葱枰獑卫J?
在許多時(shí)候,一個(gè)系統(tǒng)只需要一個(gè)全局對(duì)象,這樣有利于我們協(xié)調(diào)系統(tǒng)的整體行為。比如在某個(gè)服務(wù)器程序中,該服務(wù)器的配置信息存放在一個(gè)文件中,這些配置數(shù)據(jù)由一個(gè)單例對(duì)象統(tǒng)一讀取,然后服務(wù)進(jìn)程中的其他對(duì)象再通過這個(gè)單例對(duì)象獲取這些配置信息。這樣做可以簡(jiǎn)化對(duì)系統(tǒng)的管理,并且可以避免出現(xiàn)不一致的狀況。
如何解決 : 單例模式的概念
在Java中,單例模式就是確保一個(gè)類中只有一個(gè)實(shí)例,并且它可以提供一個(gè)全局的訪問點(diǎn)以訪問該實(shí)例。它可以為整個(gè)系統(tǒng)提供一個(gè)全局訪問點(diǎn),避免了(不必要的)頻繁的創(chuàng)建與銷毀對(duì)象,可以提高系統(tǒng)性能,節(jié)省資源,并且便于整體把控系統(tǒng)。
概念模型 : 在java中建立單例模式的模型
在Java中,萬物皆對(duì)象,那么,如何將單例模式的概念抽象為Java中的對(duì)象呢?
將單例模式看作一類對(duì)象,則這類對(duì)象需要有三大要素:
- 指向自己(唯一)實(shí)例的私有靜態(tài)引用 :因?yàn)橹挥幸粋€(gè)唯一的實(shí)例,所以用靜態(tài)引用
- 私有的構(gòu)造方法 : 避免外部創(chuàng)建實(shí)例對(duì)象
- 公有的以自己(唯一)實(shí)例為返回值的靜態(tài)方法:提供一個(gè)訪問該實(shí)例的方法
類的實(shí)現(xiàn) :
單例模式的實(shí)現(xiàn)有兩種比較經(jīng)典的實(shí)現(xiàn)方法
單線程實(shí)現(xiàn) :
餓漢模式 :
class Hunger_Thread{// 指向自己實(shí)例的私有靜態(tài)引用,// 在類加載時(shí)便new了一個(gè)對(duì)象(類只會(huì)加載一次,所以只會(huì)new一個(gè)對(duì)象)private static Hunger_Thread hunger = new Hunger_Thread();// 私有構(gòu)造方法private Hunger_Thread(){};// 以自己實(shí)例為返回值的靜態(tài)公有方法public static Hunger_Thread getHunger(){return hunger;}}懶漢模式 :
class Lazy_Thread{// 指向自己實(shí)例的私有靜態(tài)引用private static Lazy_Thread lazy;// 私有構(gòu)造方法private Lazy_Thread(){}// 以自己實(shí)例為返回的靜態(tài)公有方法public static Lazy_Thread getLazy(){if(lazy==null){lazy = new Lazy_Thread();}return lazy;} }多線程實(shí)現(xiàn) :
多線程需要考慮線程安全問題 ,由于餓漢模式(不管是單線程還是多線程)只會(huì)在類加載時(shí)new對(duì)象,由于一個(gè)類在整個(gè)生命周期中只會(huì)被加載一次,因此該單例類只會(huì)創(chuàng)建一個(gè)實(shí)例,故它是線程安全的。
但懶漢模式會(huì)出現(xiàn)線程不安全的例子 :(就拿單線程的懶漢代碼為例)
假設(shè)有線程 t1,t2,這兩個(gè)線程都調(diào)用了getLazy()方法(lazy==null,t1先執(zhí)行),t1執(zhí)行到 if(lazy==null){...}(剛判斷完,還未創(chuàng)建實(shí)例)時(shí),時(shí)間片用完了,此時(shí)該t2執(zhí)行,t2完整的執(zhí)行了該方法(已經(jīng)創(chuàng)建了實(shí)例),又該t1執(zhí)行了,此時(shí)它并不知道已經(jīng)創(chuàng)建了實(shí)例,所以還會(huì)創(chuàng)建一個(gè),這就與該模式的設(shè)計(jì)理念不符,也就是線程不安全。
解決方法也很簡(jiǎn)單,加鎖就可以了
class Lazy_Thread{// 指向自己實(shí)例的私有靜態(tài)引用private static Lazy_Thread lazy;// 私有構(gòu)造方法private Lazy_Thread(){}// 以自己實(shí)例為返回的靜態(tài)公有方法public static synchronized Lazy_Thread getLazy(){if(lazy==null){lazy = new Lazy_Thread();}return lazy;} }這樣可以解決線程安全問題,但是效率低下,鎖的粒度太大。下面的代碼段與上面的相比沒什么效率的提升。
class Lazy_Thread{// 指向自己實(shí)例的私有靜態(tài)引用private static Lazy_Thread lazy;// 私有構(gòu)造方法private Lazy_Thread(){}// 以自己實(shí)例為返回的靜態(tài)公有方法public static Lazy_Thread getLazy(){synchronized (Lazy_Thread.class){if(lazy==null){lazy = new Lazy_Thread();}}return lazy;} }一般,我們會(huì)使用雙重檢查來實(shí)現(xiàn)線程安全,這是一種比較高效的做法。
class Lazy_Thread{// 要用 volatile 修飾,防止指令重排序private static volatile Lazy_Thread lazy;private Lazy_Thread(){}// 雙重檢查實(shí)現(xiàn)線程安全public static Lazy_Thread getLazy(){if(lazy==null){// 只有第一次創(chuàng)建實(shí)例時(shí)才加鎖synchronized (Lazy_Thread.class){if(lazy==null){lazy = new Lazy_Thread();}}}return lazy;} }為什么要用 volatile 修飾 lazy ?
?new AnyClass();是一個(gè)非原子操作 ,大致會(huì)分為這三步
如果發(fā)生了指令重排序,則有可能把步驟3放在步驟2之前執(zhí)行(lazy不為null,但是它沒有初始化,它就是一個(gè)殘缺的對(duì)象)。
假設(shè)線程t1在執(zhí)行new的時(shí)候發(fā)生了指令重排序,new的步驟變成了1,3,2,而t1在執(zhí)行完3后時(shí)間片剛好用完(此時(shí) lazy 就是一個(gè)殘缺的對(duì)象)這時(shí),t2進(jìn)入線程,判斷 lazy != null 之間返回的殘缺的對(duì)象,這也是線程不安全的!
總結(jié)
- 上一篇: Opening My World——ES
- 下一篇: HALCON示例程序max_connec