日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

【多线程经典实例】实现一个线程安全的单例模式

發(fā)布時(shí)間:2023/12/10 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【多线程经典实例】实现一个线程安全的单例模式 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

文章目錄

    • 1.什么是單例模式
    • 2. 單例模式的組成
    • 3.餓漢模式實(shí)例
      • 3.1在餓漢模式中為什么在創(chuàng)建實(shí)例的時(shí)候使用static修飾?
      • 3.2 判斷該實(shí)例是否是線程安全的
    • 4.懶漢模式實(shí)例
      • 4.1 判斷該實(shí)例是否是線程安全的,如果不是線程安全的,那么怎樣修改可以成為線程安全的實(shí)例
    • 總結(jié)一下:

1.什么是單例模式

單例模式是設(shè)計(jì)模式中的一種,其實(shí)設(shè)計(jì)模式就好好比是一個(gè)棋譜,我們?cè)谌粘O缕宓臅r(shí)候會(huì)有一些經(jīng)典的套路。那么在設(shè)計(jì)模式中也有這樣的經(jīng)典套路。這些經(jīng)典的套路都是有大佬前輩們實(shí)現(xiàn)的。我們?cè)趯懘a的時(shí)候,有很多經(jīng)典的場(chǎng)景,在經(jīng)典場(chǎng)景中有一些經(jīng)典的應(yīng)對(duì)套路。大佬們把這些常見的應(yīng)對(duì)手段給整理起來,就起來個(gè)名字–設(shè)計(jì)模式。有了設(shè)計(jì)模式,無論是新手程序員還是資深的老程序員,都會(huì)有一個(gè)代碼編程規(guī)范。以便讓初出茅廬的新手,代碼不至于寫的很糟糕。

2. 單例模式的組成

單例模式分為:餓漢模式懶漢模式。單例模式之所以被稱為單例模式,是因?yàn)槲覀冊(cè)趧?chuàng)建單例模式類的時(shí)候,就把該類的構(gòu)造方法使用private進(jìn)行修飾,以便在該類外,不能直接創(chuàng)建出一個(gè)實(shí)例。

3.餓漢模式實(shí)例

餓漢模式:指的是在單例模式中,在對(duì)單例進(jìn)行初始化的時(shí)候,直接賦予單例實(shí)例,直接new出一個(gè)對(duì)象。

餓漢模式也可以這樣理解:我們平時(shí)在自己家里的時(shí)候,都洗過碗吧。就比如說,中午這頓飯使用了4個(gè)碗,在吃完飯后,我們立即就把4個(gè)碗給刷了。這里之所以被稱為餓漢模式是因?yàn)?#xff0c;在餓漢模式中,創(chuàng)建實(shí)例的時(shí)候比較著急。在初始化的時(shí)候,直接創(chuàng)建實(shí)例。

餓漢模式代碼案例:

//創(chuàng)建出一個(gè)單例模式//單例模式分為餓漢模式和懶漢模式 class Singleton{//在餓漢模式中,在進(jìn)行初始化的時(shí)候,直接創(chuàng)建出實(shí)例public static Singleton instance = new Singleton();//使用private 修飾該類的構(gòu)造方法,在類外無法創(chuàng)建出一個(gè)該類的對(duì)象private Singleton(){}//在類外調(diào)用該類中的實(shí)例public static Singleton getInstance(){return instance;} } public class TestDemo2 {public static void main(String[] args) {//Singleton singleton = new Singleton();Singleton single = Singleton.getInstance();} }

我們嘗試在main類中,自己創(chuàng)建出一個(gè)SingleTon的實(shí)例。

3.1在餓漢模式中為什么在創(chuàng)建實(shí)例的時(shí)候使用static修飾?

因?yàn)?static 修飾的成員更準(zhǔn)確的說是類成員,類屬性、類方法,不加 static 修飾的成員準(zhǔn)確的來說,就是實(shí)例方法,實(shí)例成員,實(shí)例屬性。

在一個(gè)java程序中,一個(gè)類方法只會(huì)存在一份(JVM保證的) 這也就是為什么要使用static對(duì)實(shí)例進(jìn)行修飾的原因。進(jìn)一步的就保證了在類的static 成員也只會(huì)存在一份。

在這里我們?cè)谏罹恳粋€(gè)static 關(guān)鍵字

其實(shí)在我們使用的編程語言java中,static表示的意思和這個(gè)單詞的字面意思完全不同,static 的意思大家知道 是靜態(tài)的。這其實(shí)是一個(gè)歷史遺留問題。

在C語言中的static有3個(gè)作用:

  • 修飾局部變量,把局部變量的生命周期變長(zhǎng)。修飾一個(gè)全局變量,把這個(gè)全局變量的作用域限制到整個(gè).c文件。

  • 修飾一個(gè)函數(shù),把這個(gè)函數(shù)的作用域限制到整個(gè).c文件。

    我們?cè)谶@里也可以看出在c語言中static 關(guān)鍵字的英語本意和在c語言中的使用效果,也是對(duì)不上號(hào)的。

    其實(shí)在上古時(shí)期,那時(shí)候的static是表示把變量放到靜態(tài)內(nèi)存區(qū)中,于是引入了static關(guān)鍵字,但是隨著計(jì)算機(jī)的發(fā)展,這個(gè)東西就逐漸的沒落了。但是static 關(guān)鍵字有被賦予了新的功能。

    在C++中 static關(guān)鍵字除了上述C語言的static 功能之外還有新的用法,修飾一個(gè)類的成員變量和成員函數(shù),此處static 修飾的成員就表示為類成員。

    Java語言就是把C++中static 的功能繼承過來了而已。

既然static 關(guān)鍵字的本意和它的對(duì)應(yīng)效果對(duì)不上號(hào),那么為什么不使用其他的詞呢?

在一個(gè)編程語言中,要想新增一個(gè)關(guān)鍵字,是一件非常有風(fēng)險(xiǎn)的事情。因?yàn)椴荒苓€在程序中的單詞重合。

SingleTon.class 類對(duì)象,就是.class文件被JVM加載到內(nèi)存中,表現(xiàn)出來的模樣。類對(duì)象就有著.class文件的所有信息。就像類名,屬性等都可以有SingleTon.class中找到。這樣也就實(shí)現(xiàn)了反射

3.2 判斷該實(shí)例是否是線程安全的

餓漢模式是線程安全的

那么為什么餓漢模式樣式的單例模式是線程安全的呢?我們?cè)诔绦虻哪睦锱袛嘣搯卫J绞蔷€程安全的?

線程安不安全,具體是在多線程環(huán)境之下,并發(fā)調(diào)用的getInstance()方法是否會(huì)產(chǎn)生bug?

在博主的上一篇文章中,介紹了產(chǎn)生線程不安全的案例。

造成線程不安全的案例有5種。

  • 線程搶占式執(zhí)行,線程間的調(diào)度充滿了隨機(jī)性。
  • 多線程對(duì)一個(gè)變量進(jìn)行修改。
  • 針對(duì)變量操作不是原子的
  • 內(nèi)存可見性問題
  • 指令重排序問題。
  • 我們現(xiàn)在回顧在餓漢模式中的getInstance()方法,在該方法中只有一個(gè)return操作,就是對(duì)一個(gè)變量進(jìn)行了讀取,符合針對(duì)變量操作的原子性。所以是線程安全的

    4.懶漢模式實(shí)例

    懶漢模式創(chuàng)建懶漢模式的單例模式的時(shí)候,我們不著急創(chuàng)建出實(shí)例。

    還是那洗碗舉例,我們中午吃飯的時(shí)候,使用了4個(gè)碗,吃完飯后,我們不著急洗碗,到了晚上吃飯的時(shí)候,需要使用的幾個(gè)碗,那么我們現(xiàn)在就洗幾個(gè)碗。假如說我們晚上要使用2個(gè)碗,那么就洗兩個(gè),剩下的兩個(gè)碗不管。

    在我們平時(shí)生活中,餓漢模式比懶漢模式好,因?yàn)槟阍囈辉囍形绯酝觑埐幌赐?#xff0c;如果不洗碗肯定會(huì)被挨罵。但是在我們的計(jì)算機(jī)中可未必,懶漢模式要比餓漢模式要好一些。

    那么為什么在計(jì)算機(jī)中懶漢模式要比餓漢模式要好一些呢?

    就比如說,現(xiàn)在有一個(gè)1G的圖片文件,如果按照餓漢模式,那么計(jì)算機(jī)就會(huì)在內(nèi)存中一下把這1G大的圖片文件全部加載出來,這不就耗費(fèi)CUP資源,并且如果計(jì)算機(jī)用戶在瀏覽圖片文件的時(shí)候,就看了一點(diǎn)沒有全部把圖片文件瀏覽完。那么這樣的餓漢模式不就是費(fèi)力不討好好嗎?

    反而看看我們的懶漢模式,之所以懶,是因?yàn)槟阙s它一些它走一下。在懶漢模式中,我們一次不會(huì)再內(nèi)存中把所有的圖片加載完,而是把計(jì)算機(jī)用戶的一個(gè)計(jì)算機(jī)屏幕中的圖片加載出來。用戶滑動(dòng)一下滾動(dòng)條,加載一下。這樣就進(jìn)行了優(yōu)化。

    其實(shí)在計(jì)算機(jī)中懶漢模式是褒義詞,但是在現(xiàn)實(shí)世界中就算了吧😂

    class Singleton2{//懶漢模式,在該模式中不著急創(chuàng)建出實(shí)例,在類外需要的時(shí)候,我們?cè)龠M(jìn)行創(chuàng)建public static Singleton2 instance = null;private Singleton2(){}public static Singleton2 getInstance(){if(instance == null){instance = new Singleton2();}return instance;} } public class TestDemo3 {public static void main(String[] args) {Singleton2 singleton2 = Singleton2.getInstance();} }

    4.1 判斷該實(shí)例是否是線程安全的,如果不是線程安全的,那么怎樣修改可以成為線程安全的實(shí)例

    首先上述的所謂的懶漢模式的單例模式不是線程安全的

    那么為什么它不是線程安全的呢?

    因?yàn)樵诙嗑€程中,我們調(diào)用懶漢模式中的getInstance()方法的時(shí)候,針對(duì)變量的操作不是原子的,那么有從哪可以看出不是原子的呢?

    如圖:

    那么針對(duì)變量操作不是原子性的,它的解決辦法就是進(jìn)行加鎖,使用synchronized關(guān)鍵字進(jìn)行加鎖!!!

    修改之后的代碼:

    //實(shí)現(xiàn)一個(gè)線程安全的單例模 class Singleton2{//懶漢模式,在該模式中不著急創(chuàng)建出實(shí)例,在類外需要的時(shí)候,我們?cè)龠M(jìn)行創(chuàng)建public static Singleton2 instance = null;private Singleton2(){}public static Singleton2 getInstance(){synchronized(Singleton2.class) {if (instance == null) {instance = new Singleton2();}}return instance;} } public class TestDemo3 {public static void main(String[] args) {Singleton2 singleton2 = Singleton2.getInstance();} }

    我們知道如果遇到可針對(duì)變量操作不是原子的,要使用synchronized關(guān)鍵字進(jìn)行加鎖,但是也不是說,代碼中有了synchroniezd關(guān)鍵字就一定不會(huì)線程安全,我們要把synchronized關(guān)鍵字加對(duì)地方。synchronized加的位置正確,不能隨便寫。

    //類對(duì)象在一個(gè)類中只有唯一一份,就能保證調(diào)用的getInstance的時(shí)候都是針對(duì)都一個(gè)對(duì)象進(jìn)行加鎖 synchronized(SingleTon2.class){}

    但是我們加鎖之后,又帶來的新的問題!!!

    對(duì)于剛才的這個(gè)懶漢模式的代碼而言,線程不安全發(fā)生在instance沒有被初始化之前,未被初始化的時(shí)候,多線程調(diào)用getInstance()方法時(shí),會(huì)存在線程安全問題,因?yàn)樯婕暗阶x和修改。但是在instance初始化之后,instance一定不是null,if條件一定不成立,getInstance()就只剩下兩個(gè)讀操作,也就是說instance初始化之后,線程就是安全的了。

    并且按照上述的加鎖操作,無論是代碼中的instance初始化之前,還是初始化之后。每次調(diào)用getInstance()方法的時(shí)候,都會(huì)對(duì)其進(jìn)行加鎖。也就意味著即使初始化之后(已經(jīng)線程安全了),但是仍然存在大量的鎖競(jìng)爭(zhēng)。

    既然這里的instance已經(jīng)被初始化過了,即使這里的條件在不能被滿足了,但是仍然會(huì)調(diào)用getInstance()方法,都需要進(jìn)行加鎖,也就可能會(huì)產(chǎn)生鎖競(jìng)爭(zhēng),但是我們知道這里的鎖競(jìng)爭(zhēng)其實(shí)是沒有必要的。

    我們知道加鎖確實(shí)能讓線程安全,但是同時(shí)也付出了代價(jià),一旦在一個(gè)線程中加了鎖之后,那么就和運(yùn)行高效無關(guān)了。(程序的速度就變慢了)因?yàn)榧渔i之后,線程之間是串行執(zhí)行的。代碼的運(yùn)行效率就變慢了。

    博主以前說過開發(fā)效率要比運(yùn)行效率更重要,一切都要從程序員的利益出發(fā),但是運(yùn)行效率也不是說不重要!如果說運(yùn)行效率不重要的話,那么我們?cè)谇懊鎸W(xué)習(xí)那么多的數(shù)據(jù)結(jié)構(gòu)干啥,不都是使用一個(gè)較好的數(shù)據(jù)結(jié)構(gòu),來組織數(shù)據(jù),讓代碼變得有效嘛

    改進(jìn)方案:

    在instance初始化之前,才進(jìn)行加鎖,在初始化之后,不進(jìn)行加鎖。在加鎖這里在加一個(gè)條件判斷即可

    代碼如下:

    //實(shí)現(xiàn)一個(gè)線程安全的單例模 class Singleton2{//懶漢模式,在該模式中不著急創(chuàng)建出實(shí)例,在類外需要的時(shí)候,我們?cè)龠M(jìn)行創(chuàng)建public static Singleton2 instance = null;private Singleton2(){}public static Singleton2 getInstance(){if(instance == null) { //如果instance被初始化過了,那么就不必再進(jìn)行加鎖,直接返回這個(gè)實(shí)例即可synchronized (Singleton2.class) {if (instance == null) {instance = new Singleton2();}}}return instance;} } public class TestDemo3 {public static void main(String[] args) {Singleton2 singleton2 = Singleton2.getInstance();} }

    我們?cè)谏鲜龅拇a中,可以看到在getInstance()方法中,使用了兩個(gè)條件判斷語句,都是判斷instance==null ,但是這兩個(gè)條件判斷語句的實(shí)際含義是千差萬別。這兩個(gè)添加判斷長(zhǎng)得一樣,純屬是一個(gè)美麗的錯(cuò)誤。

    上面的條件判斷是是否需要加鎖。也就是說現(xiàn)在的instance是否已經(jīng)被初始化過了

    下面的條件判斷是是否需要?jiǎng)?chuàng)建實(shí)例。

    我們?nèi)绻サ袅死飳拥臈l件判斷語句那么就會(huì)變成:

    //實(shí)現(xiàn)一個(gè)線程安全的單例模 class Singleton2{//懶漢模式,在該模式中不著急創(chuàng)建出實(shí)例,在類外需要的時(shí)候,我們?cè)龠M(jìn)行創(chuàng)建public static Singleton2 instance = null;private Singleton2(){}public static Singleton2 getInstance(){if(instance == null) {synchronized (Singleton2.class) { //其實(shí)在這里加鎖,就是加了個(gè)寂寞,在這里只針對(duì)設(shè)置實(shí)例加鎖,在加鎖語句的外面,還有istance == null 涉及到 讀和判斷,所以說加鎖和沒加一樣,還是不符合原子性。instance = new Singleton2();}}return instance;} } public class TestDemo3 {public static void main(String[] args) {Singleton2 singleton2 = Singleton2.getInstance();} }

    如果直接對(duì)getInstance()方法進(jìn)行加鎖,那么就是一個(gè)無腦加鎖

    此處博主告訴各位老鐵,在上述的代碼中,還存在一個(gè)問題。但是已經(jīng)線程安全了呀,哪里還有錯(cuò)呢?我們?cè)趩卫J街惺褂胿olatile,主要是使用volatile可以進(jìn)制指令重排序,從而保證程序的正常運(yùn)行。

    public class Singleton {private Singleton() {}// 使用 volatile 禁止指令重排序private static volatile Singleton instance = null;public static Singleton getInstance() {if (instance == null) { // 1synchronized (Singleton.class) {if (instance == null) {instance = new Singleton(); // 2}}}return instance;} }

    注意觀察上述代碼,我標(biāo)記了第 ① 處和第 ② 處的兩行代碼。給私有變量加 volatile 主要是為了防止第 ② 處執(zhí)行時(shí),也就是“instance = new Singleton()”執(zhí)行時(shí)的指令重排序的,這行代碼看似只是一個(gè)創(chuàng)建對(duì)象的過程,然而它的實(shí)際執(zhí)行卻分為以下 3 步:

    創(chuàng)建內(nèi)存空間。
    在內(nèi)存空間中初始化對(duì)象 Singleton。
    將內(nèi)存地址賦值給 instance 對(duì)象(執(zhí)行了此步驟,instance 就不等于 null 了)。

    試想一下,如果不加 volatile,那么線程 1 在執(zhí)行到上述代碼的第 ② 處時(shí)就可能會(huì)執(zhí)行指令重排序,將原本是 1、2、3 的執(zhí)行順序,重排為 1、3、2。但是特殊情況下,線程 1 在執(zhí)行完第 3 步之后,如果來了線程 2 執(zhí)行到上述代碼的第 ① 處,判斷 instance 對(duì)象已經(jīng)不為 null,但此時(shí)線程 1 還未將對(duì)象實(shí)例化完,那么線程 2 將會(huì)得到一個(gè)被實(shí)例化“一半”的對(duì)象,從而導(dǎo)致程序執(zhí)行出錯(cuò),這就是為什么要給私有變量添加 volatile 的原因了。

    總結(jié)一下:

    實(shí)現(xiàn)一個(gè)線程安全的單例模式—針對(duì)懶漢模式

  • 在正確的位置加鎖
  • 雙重if判定
  • volatile關(guān)鍵字
  • 總結(jié)

    以上是生活随笔為你收集整理的【多线程经典实例】实现一个线程安全的单例模式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。