并发编程-09安全发布对象+单例模式详解
文章目錄
- 腦圖
- 概述
- 安全發(fā)布對象的4種方式
- 示例
- 懶漢模式(線程不安全)
- 餓漢模式 靜態(tài)域(線程安全)
- 改造線程不安全的懶漢模式方式一 靜態(tài)方法使用synchronized修飾 (線程安全)
- 改造線程不安全的懶漢模式方式二雙重檢查機制(線程不安全)
- 改造線程不安全的懶漢模式方式二雙重檢查機制優(yōu)化-volatile + 雙重檢測機制 (線程安全)
- 餓漢模式的第二種寫法 靜態(tài)代碼塊 (線程安全)
- 餓漢模式的第三種寫法 靜態(tài)內部類 (線程安全)
- 小結
- 枚舉模式 推薦 ( 線程安全,防止反射構建)
- 代碼
腦圖
概述
上篇文章并發(fā)編程-08安全發(fā)布對象之發(fā)布與逸出中簡單的描述了下對象發(fā)布和逸出的概念,并通過demo演示了不安全發(fā)布對象對象逸出(this引用逸出)。 那該如何安全的發(fā)布對象呢?
安全發(fā)布對象的4種方式
-
在靜態(tài)初始化函數(shù)中初始化一個對象的引用
-
將對象的引用保存到volatile類型域或者AtomicReference對象中
-
將對象的引用保存到某個正確構造對象的final類型域中
-
將對象的引用保存到一個由鎖保護的域中
示例
上面所提到的幾種方法都可以應用到單例模式中,我們將以單例模式為例,介紹如何安全發(fā)布對象,以及單例實現(xiàn)的一些注意事項。
以前寫的一篇文章: 單例模式
懶漢模式(線程不安全)
package com.artisan.example.singleton;import com.artisan.anno.NotThreadSafe;/*** 懶漢模式 單例的實例在第一次調用的時候創(chuàng)建* * 單線程下沒問題,多線程下getInstance方法線程不安全* * @author yangshangwei**/ @NotThreadSafe public class SingletonLazyModel {// 私有構造函數(shù)// 如果要保證一個類只能被初始化一次,首先要保證的是構造函數(shù)是私有的,不允許外部類直接調用new方法private SingletonLazyModel() {// 可以初始化一些資源等}// static單例對象private static SingletonLazyModel instance = null;// 靜態(tài)工廠方法 // public方法外部通過getInstance獲取public static SingletonLazyModel getInstance() {// 多線程情況下,假設線程A和線程B同時獲取到instance為null, 這時候instance會被初始化兩次if (instance == null) {instance = new SingletonLazyModel();}return instance;}}餓漢模式 靜態(tài)域(線程安全)
package com.artisan.example.singleton;import com.artisan.anno.ThreadSafe;/*** 餓漢模式 單例的實例在類裝載的時候進行創(chuàng)建* * 因為是在類裝載的時候進行創(chuàng)建,可以確保線程安全* * * 餓漢模式需要注意的地方: 1.私有構造函數(shù)中不要有太多的邏輯,否則初始化會慢 2.確保初始化的對象能夠被使用,否則造成資源浪費* * @author yangshangwei**/ @ThreadSafe public class SingletonHungerModel {// 私有構造函數(shù)// 如果要保證一個類只能被初始化一次,首先要保證的是構造函數(shù)是私有的,不允許外部類直接調用new方法private SingletonHungerModel() {// 可以初始化一些資源等}// static單例對象 靜態(tài)域private static SingletonHungerModel instance = new SingletonHungerModel();// public方法外部通過getInstance獲取public static SingletonHungerModel getInstance() {// 直接返回實例化后的對象return instance;}}改造線程不安全的懶漢模式方式一 靜態(tài)方法使用synchronized修飾 (線程安全)
僅需要將靜態(tài)的 getInstance方法使用synchronized修飾即可,但是缺點也很明顯,線程阻塞,效率較低
synchronized修飾靜態(tài)方法的作用域及demo見 并發(fā)編程-05線程安全性之原子性【鎖之synchronized】
改造線程不安全的懶漢模式方式二雙重檢查機制(線程不安全)
改造線程不安全的懶漢模式方式一 靜態(tài)方法使用synchronized修飾的缺點既然都清楚了,為了提高效率,那就把synchronized下沉到方法中的實現(xiàn)里吧
package com.artisan.example.singleton;import com.artisan.anno.NotThreadSafe;/*** 懶漢模式 單例的實例在第一次調用的時候創(chuàng)建* * 對static getInstance方法 進行 雙重檢測* * @author yangshangwei**/ @NotThreadSafe public class SingletonLazyModelOptimize2 {// 私有構造函數(shù)// 如果要保證一個類只能被初始化一次,首先要保證的是構造函數(shù)是私有的,不允許外部類直接調用new方法private SingletonLazyModelOptimize2() {// 可以初始化一些資源等}// static單例對象private static SingletonLazyModelOptimize2 instance = null;// 靜態(tài)工廠方法// public方法外部通過getInstance獲取public static SingletonLazyModelOptimize2 getInstance() {// 多線程情況下,假設線程A和線程B同時獲取到instance為null, 這時候instance會被初始化兩次,所以在判斷中加入synchronizedif (instance == null) {// synchronize修飾類 ,修飾范圍是synchronized括號括起來的部分,作用于所有對象synchronized(SingletonLazyModelOptimize2.class) {if (instance == null) {instance = new SingletonLazyModelOptimize2();}}}return instance;}}先說下結論: 上述代碼是線程不安全的,可能會返回一個未被實例化的instance,導致錯誤。
這個就要從cpu的指令說起了。
問題主要出在實例化這一步
instance = new SingletonLazyModelOptimize2()這個實例化的操作,對應底層3個步驟
對于單線程,肯定是沒有問題的。但是對于多線程,CPU為了執(zhí)行效率,可能會發(fā)生指令重排序。
經過JVM和CPU的優(yōu)化,因為第2步和第2步本質上沒有先后關系,指令可能會重排成下面的順序 1—>3—>2:
- 1.memory = allocate() // 分配對象的內存空間
- 3.instance = memory // 設置instance指向剛分配的內存
- 2.ctorInstance() // 初始化對象
假設按照這個指令順序執(zhí)行的話,那么當線程A執(zhí)行完1和3時,instance對象還未完成初始化,但已經不再指向null。此時如果線程B搶占到CPU資源,執(zhí)行 if (instance == null)的結果會是false,從而返回一個沒有初始化完成的instance對象
改造線程不安全的懶漢模式方式二雙重檢查機制優(yōu)化-volatile + 雙重檢測機制 (線程安全)
經過volatile的修飾,保證變量的可見性,當線程A執(zhí)行instance = new SingletonLazyModelOptimize3的時候,JVM執(zhí)行順序會始終保證是下面的順序:
- 1.memory = allocate() // 分配對象的內存空間
- 2.ctorInstance() // 初始化對象
- 3.instance = memory // 設置instance指向剛分配的內存
這樣的話線程B看來,instance對象的引用要么指向null,要么指向一個初始化完畢的Instance,而不會出現(xiàn)某個中間態(tài),保證了線程安全。
餓漢模式的第二種寫法 靜態(tài)代碼塊 (線程安全)
見注釋
package com.artisan.example.singleton;import com.artisan.anno.ThreadSafe;/*** 餓漢模式 單例的實例在類裝載的時候進行創(chuàng)建* * 因為是在類裝載的時候進行創(chuàng)建,可以確保線程安全* * * 餓漢模式需要注意的地方: 1.私有構造函數(shù)中不要有太多的邏輯,否則初始化會慢 2.確保初始化的對象能夠被使用,否則造成資源浪費* * @author yangshangwei**/ @ThreadSafe public class SingletonHungerModel2 {// 私有構造函數(shù)// 如果要保證一個類只能被初始化一次,首先要保證的是構造函數(shù)是私有的,不允許外部類直接調用new方法private SingletonHungerModel2() {// 可以初始化一些資源等}// 注意: static的順序不要寫反了,否則會拋空指針。 static的加載順序是按順序執(zhí)行// static單例對象 靜態(tài)域private static SingletonHungerModel2 instance = null;// 靜態(tài)塊static {instance = new SingletonHungerModel2();}// public方法外部通過getInstance獲取public static SingletonHungerModel2 getInstance() {// 直接返回實例化后的對象return instance;}}餓漢模式的第三種寫法 靜態(tài)內部類 (線程安全)
package com.artisan.example.singleton;import com.artisan.anno.ThreadSafe;/*** 餓漢模式 單例的實例在類裝載的時候進行創(chuàng)建* * 使用靜態(tài)內部類實現(xiàn)的單例模式-線程安全* * * 餓漢模式需要注意的地方: 1.私有構造函數(shù)中不要有太多的邏輯,否則初始化會慢 2.確保初始化的對象能夠被使用,否則造成資源浪費* * @author yangshangwei**/ @ThreadSafe public class SingletonHungerModel3 {// 私有構造函數(shù)// 如果要保證一個類只能被初始化一次,首先要保證的是構造函數(shù)是私有的,不允許外部類直接調用new方法private SingletonHungerModel3() {// 可以初始化一些資源等}// 靜態(tài)工廠方法-獲取實例public static SingletonHungerModel3 getInstance() {// 直接返回實例化后的對象return InstanceHolder.INSTANCE;}// 用靜態(tài)內部類創(chuàng)建單例對象 private 修飾private static class InstanceHolder {private static final SingletonHungerModel3 INSTANCE = new SingletonHungerModel3();}}注意事項
- 從外部無法訪問靜態(tài)內部類InstanceHolder (private修飾的),只有當調用Singleton.getInstance方法的時候,才能得到單例對象instance。
- instance對象初始化的時機并不是在單例類Singleton被加載的時候,而是在調用getInstance方法,使得靜態(tài)內部類InstanceHolder 被加載的時候。因此這種實現(xiàn)方式是利用classloader的加載機制來實現(xiàn)懶加載,并保證構建單例的線程安全。
小結
小結: 以上所提到的單例實現(xiàn)方式并不能算是完全安全的,這里的安全不僅指線程安全還有發(fā)布對象的安全。因為以上例子所實現(xiàn)的單例模式,我們都可以通過反射機制去獲取私有構造器更改其訪問級別從而實例化多個不同的對象。
那么如何防止利用反射構建對象呢?這時我們就需要使用到內部枚舉類了,因為JVM可以阻止反射獲取枚舉類的私有構造方法。
枚舉模式 推薦 ( 線程安全,防止反射構建)
package com.artisan.example.singleton;import lombok.Getter;public class SingletonEum {/*** 私有構造函數(shù)*/private SingletonEum() {}/*** 靜態(tài)工廠方法-獲取實例** @return instance*/public static SingletonEum getInstance() {return Singleton.INSTANCE.getInstance();}/*** 由枚舉類創(chuàng)建單例對象*/@Getterprivate enum Singleton {INSTANCE;/*** 單例對象*/private SingletonEum instance;/*** JVM保證這個方法絕對只調用一次*/Singleton() {instance = new SingletonEum();}}}使用枚舉實現(xiàn)的單例模式,不但可以防止利用反射強行構建單例對象,而且可以保證線程安全,并且可以在枚舉類對象被反序列化的時候,保證反序列的返回結果是同一對象。
上面代碼中之所以使用內部枚舉類的原因是為了讓這個單例對象可以懶加載,相當于是結合了靜態(tài)內部類的實現(xiàn)思想。若不使用內部枚舉類的話,單例對象就會在枚舉類被加載的時候被構建。
代碼
https://github.com/yangshangwei/ConcurrencyMaster
總結
以上是生活随笔為你收集整理的并发编程-09安全发布对象+单例模式详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 并发编程-08安全发布对象之发布与逸出
- 下一篇: 并发编程-10线程安全策略之不可变对象