多線程案例
- 8.1 單例模式
- 餓漢模式
- 懶漢模式
- 懶漢模式-單線程版
- 懶漢模式-多線程版
- 懶漢模式-多線程版(改進)
8.1 單例模式
單個實例. 在一個 java 進程中, 要求指定的類,只能有唯–個實例。(嘗試 new 多個實例的時候, 就會直接編譯報錯)
單例模式是校招中最常考的設計模式之?.
啥是設計模式?
設計模式好?象棋中的 “棋譜”. 紅?當頭炮, ???來跳. 針對紅?的?些?法, ??應招的時候有?些固定的套路. 按照套路來?局勢就不會吃虧.
軟件開發中也有很多常?的 “問題場景”. 針對這些問題場景, ?佬們總結出了?些固定的套路. 按照這個套路來實現代碼, 也不會吃虧.
單例模式能保證某個類在程序中只存在唯??份實例, ?不會創建出多個實例.
這?點在很多場景上都需要. ?如 JDBC 中的 DataSource 實例就只需要?個.
什么場景適合使用單例模式?
代碼中的有些對象,本身就不應該是有多個實例的.從業務角度就應該是單個實例.
單例模式具體的實現?式有很多. 最常?的是 “餓漢” 和 “懶漢” 兩種.
餓漢模式
類加載的同時, 創建實例.
class Singleton {private static Singleton instance = new Singleton();private Singleton() {}public static Singleton getInstance() {return instance;}
}
在這個類被加載的時候,就會初始化這個 靜態成員。實例創建的時機非常早,就使用"餓漢!
萬一,其他代碼又 new 了這個類的實例咋辦呢?需要禁止外部代碼來創建該類的實例
雖然構造方法是 private,但是能否在類外面通過 反射 拿到私有構造方法創建實例??
原則上來說,可以做到。但是, 實際開發中, 反射不敢亂用的!!!反射屬于非常規的編程,特殊場景下的特殊解決方案!!!! 使用反射要付出很大的代價(會嚴重影響代碼的可讀性和封裝性)
類似于,通常情況下,你肯定沒法直接闖入別人家里。但是, 你不能進, 不代表jc 蜀黍不能進,如果jc 蜀黍到處亂闖,當然也是不行的
代碼中隨便濫用反射,是非常糟糕的~~
懶漢模式
懶漢模式-單線程版
類加載的時候不創建實例. 第?次使?的時候才創建實例.
class Singleton {private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}
在計算機中,懶 的思想,就非常有意義
如果代碼中存在多個單例類,使用餓漢模式,就會導致這些實例都是在程序啟動的時候扎堆的創建的.可能把程序啟動時間拖慢.
如果是懶漢模式,啥時候首次調用, 調用時機是分散的. 化整為零, 用戶不太容易感知到"卡頓
如果是首次調用 getlnstance, 那么此時 instance 引用為 null,就會進入 if 條件,從而把實例創建出來,如果是后續再次調用 getlnstance, 由于 instance 已經不再是 null,此時不會進入if, 直接返回之前創建好的引用了。這樣設定,仍然可以保證,該類的實例是唯一一個。與此同時,創建實例的時機就不是程序驅動時了,而是第一次調用getlnstance的時候
這個操作的執行時機就看你程序的實際需求。大概率要比餓漢這種方式要晚一些,甚至有可能整個程序壓根用不到這個方法,也就把創建的操作給省下了
有的程序, 可能是根據一定的條件,來決定是否要進行某個操作,進一步的來決定創建某個實例
比如,肯德基有個操作“瘋狂星期四”,對于 肯德基 點餐系統來說,就可以判定今天星期幾。如果是星期四,才加載 瘋狂星期四 相關的邏輯和數據,如果不是星期四,就不用加載了(節省了一定的開銷)
懶漢模式-多線程版
上述的代碼,餓漢模式和懶漢模式,是否是線程安全的?? 如果在多個線程中, 并發的調用 getlnstance, 這兩個代碼是否是線程安全的呢??
餓漢: getlnstance 直接返回 Instance 實例. 這個操作本質上就是"讀操作"。多個線程讀取同一個變量,是線程安全的!!
懶漢: 線程不安全,在多線程環境下可能會創建出多個實例!!在懶漢模式中,代碼有讀也有寫,如果 t1 和 t2 按照下列順序來執行,就會出現問題!!
上?的懶漢模式的實現是線程不安全的.
線程安全問題發?在?次創建實例時. 如果在多個線程中同時調? getInstance ?法, 就可能導致創建出多個實例.
?旦實例已經創建好了, 后?再多線程環境調? getInstance 就不再有線程安全問題了(不再修改instance 了)
加上 synchronized 可以改善這?的線程安全問題.
class Singleton {private static Singleton instance = null;private Singleton() {}public synchronized static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}
懶漢模式-多線程版(改進)
多線程代碼, 其實是非常復雜的,代碼稍微變換一點,結論就截然不同!!
因此可千萬不要以為,代碼中寫了 synchronized 就一定線程安全,不寫 synchronized 就一定線程不安全!!!
一定要具體問題具體分析.要分析這個代碼在各種調度執行順序下可能的情況,確保每個情況都是正確的!!
此處要想讓代碼執行正確,其實是需要把 if 和 new 兩個操作,打包成一個原子的!!
更加合理的做法,應該是把 synchronized 套到if 外頭~~
但上述代碼仍然存在問題~~
效率非常低!!!
如果 Instance 已經創建過了,此時后續再調用 getlnstance 就都是直接返回 Instance 實例了(此處的操作就是純粹的讀操作了,也就不會有線程安全問題了)
此時,針對這個已經沒有線程安全問題的代碼,仍然是每次調用都先加鎖再解鎖,此時,效率就非常低了!!!加鎖就意味著可能會產生阻塞,一旦線程阻塞,啥時候能解除,就不知道了(你可以認為,只要一個代碼里加鎖了,基本就注定和“高性能"無緣)
在需要加鎖的時候才加鎖,不該加鎖,不能隨便亂加。所以除了 StringBuffer 還提供 StringBuilder, 除了 Vector 還提供 ArrayList
這個代碼仍然有點問題~~
指令重排序,引起的線程安全問題
指令重排序,也是編譯器優化的一種方式,調整原有代碼的執行順序,保證邏輯不變的前提下,提高程序的效率
instance = new singletonLazy();
這行代碼,其實可以拆成三個大的步驟,(不是三個指令)
1.申請一段內存空間
2.在這個內存上調用構造方法,創建出這個實例
3.把這個內存地址賦值給 |nstance 引用變量
正常情況下,上述代碼是按照 123 的順序來執行的,但是編譯器也可能會優化成132的順序來執行,無論是123 還是132在單線程下都是可以的~~
1 就相當于是你買了個房子,2 就相當于給房子裝修,3 就相當于你拿到房子的鑰匙。123 拿到鑰匙之后,就得到了裝修好的房子. 稱為"精裝房",132你先拿鑰匙,然后自己負責裝修.稱為"毛壞房"。如果你出去買房子,這兩種情況都會存在!!!
但是, 如果是在多線程下,指令重排序,就可能引入問題了!!如果你出去買房子,這兩種情況都會存在!!!
t1 按照132 的方式來執行這里的 new 操作:
上述代碼中,由于 t1 線程執行完13之后,調度走,此時 instance 指向的是一個 非 null 的,但是未初始化的對象。此時 t2 線程判定 instance == null 不成立,就會直接 return.如果 t2 繼續使用 instance 里面的屬性或者方法,就會出現問題(此時這里的屬性都是未初始化的"全 0"值). 就可能會引起代碼的邏輯出現問題.
解決上述問題,核心思路, 還是 volatile
volatile 有兩個功能
1.保證內存可見性,每次訪問變量必須都要重新讀取內存,而不會優化到寄存器/緩存中
2.禁止指令重排序.針對這個唄 volatile 修飾的變量的讀寫操作相關指令,是不能被重排序的!!
以下代碼在加鎖的基礎上, 做出了進?步改動:
? 使?雙重 if 判定, 降低鎖競爭的頻率.
? 給 instance 加上了 volatile.
class Singleton {
private static volatile Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}
這個代碼是一個經典高頻面試題,非常重要,咱們同學們最近這幾年秋招也會經常遇到這個問題~~
這個題并不簡單。加上這三個點,怎么加,容易答上來,為啥要這么加,每個地方解決的什么問題,要想給面試官解釋清楚,沒那么容易的!!
(1)寫博客,提前梳理好你都要說啥。
(2)給面試官講的過程中,一定要多畫圖
線下面試,可以自帶紙筆;線上面試,一般面試系統也會支持畫圖功能,可以共享屏幕。有的面試系統 牛客網面試系統,自身就支持畫圖,包括 騰訊會議,也支持畫圖
多去畫!!!
目前來看線上面試越來越多,越是好的公司,越是線上面試
面試中考察的方法非常簡單:
讓你現場寫一個單例模式的代碼
這個代碼咋寫?直接就寫成現在這個模樣嘛??
正確的寫法:
1.先寫一個不帶線程安全的單例模式
2.思索片刻, 線程不安全,把鎖加上
3.再次思索片刻,加上 if(雙重 if)
4. 再次思考片刻, 加上 volatile
意味著這個題不是你提前準備好,是你現場想出來的,面試官就會覺得,你這邊很可能沒有準備過/很久之前看的,即使如此,能夠通過已經掌握的知識,推理出一些結論
一次寫出最終版本,再面試官眼里,他覺得這個問題,你正好準備過,此時說明這個題目就考察不出來啥,這題不算,談下一話題(面試的時候,大部分面試官,看到你的回答有問題的時候,都會進一步去問的)
人生如戲,全靠演技
把問題引導到你自己擅長的角度,把控整個面試的節奏~~
理解雙重 if 判定 / volatile:
加鎖 / 解鎖是?件開銷?較?的事情. ?懶漢模式的線程不安全只是發?在?次創建實例的時候. 因此后續使?的時候, 不必再進?加鎖了.
外層的 if 就是判定下看當前是否已經把 instance 實例創建出來了.
同時為了避免 “內存可?性” 導致讀取的 instance 出現偏差, 于是補充上 volatile .
當多線程?次調? getInstance, ?家可能都發現 instance 為 null, 于是?繼續往下執?來競爭鎖, 其中競爭成功的線程, 再完成創建實例的操作.
當這個實例創建完了之后, 其他競爭到鎖的線程就被?層 if 擋住了. 也就不會繼續創建其他實例.
- 有三個線程, 開始執? getInstance , 通過外層的 if (instance == null) 知道了實例還沒有創建的消息. 于是開始競爭同?把鎖.
- 其中線程1 率先獲取到鎖, 此時線程1 通過?層的 if (instance == null) 進?步確認實例是否已經創建. 如果沒創建, 就把這個實例創建出來.
- 當線程1 釋放鎖之后, 線程2 和 線程3 也拿到鎖, 也通過?層的 if (instance == null) 來確認實例是否已經創建, 發現實例已經創建出來了, 就不再創建了
- 后續的線程, 不必加鎖, 直接就通過外層 if (instance == null) 就知道實例已經創建了,從?不再嘗試獲取鎖了. 降低了開銷.