Java併發編程 (五) 線程安全性

我的博客網:https://wushaopei.github.io/    (你想要這裏多有)

1、安全發佈對象-發佈與逸出

一、發佈與逸出定義

發佈對象 : 使一個對象可以被當前範圍以外的代碼所使用html

對象逸出: 一種錯誤的發佈。當一個對象尚未構造完成時,就使它被其餘線程所見。java

二、開發工做中涉及到的發佈對象:

好比經過類的非私有方法,返回對象的引用;或者經過共有靜態變量發佈對象。git

三、發佈對象代碼演示:

package com.mmall.concurrency.example.publish; import com.mmall.concurrency.annoations.NoThreadSafe; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; /** * @ClassName UnsafePublish * @Description TODO * @Author wushaopei * @Date 2019/10/31 15:28 * @Version 1.0 */ @Slf4j @NoThreadSafe public class UnsafePublish { private String [] states = {"a","b","c"}; public String [] getStates(){ return states; } public static void main(String[] args) { UnsafePublish unsafePublish = new UnsafePublish(); log.info("{}", Arrays.toString(unsafePublish.getStates())); //對私有屬性數組進行修改 unsafePublish.getStates()[0] = "d"; log.info("{}",Arrays.toString(unsafePublish.getStates())); } }

執行結果:github

15:31:56.516 [main] INFO com.mmall.concurrency.example.publish.UnsafePublish - [a, b, c]
15:31:56.521 [main] INFO com.mmall.concurrency.example.publish.UnsafePublish - [d, b, c]

Process finished with exit code 0

分析:apache

經過public getStates方法發佈了訪問states的域,這樣任何的外部線程均可以修改這個域,這樣的發佈對象實際上是不安全的,由於沒法假設其餘線程會不會修改這個域,從而形成類裏面這個狀態的錯誤。數組

簡單來講,這裏經過new UnsafePublish()發佈了這個類的實例,而後咱們能夠經過它提供給咱們的public方法直接獲得裏面這個私有域states的引用,獲得後咱們就能夠在其餘的任何線程裏面修改這個數組裏面的值,這樣一來,當我想要使用states裏面的數據時,這時它裏面的數據就是不徹底肯定的,所以,這樣發佈的對象就是線程不安全的。安全

四、對象逸出代碼演示:

package com.mmall.concurrency.example.publish; import com.mmall.concurrency.annoations.NoRecommend; import com.mmall.concurrency.annoations.NoThreadSafe; import com.sun.org.apache.bcel.internal.classfile.InnerClass; import lombok.extern.slf4j.Slf4j; /** * @ClassName Escape * @Description TODO * @Author wushaopei * @Date 2019/10/31 15:39 * @Version 1.0 */ @Slf4j @NoThreadSafe @NoRecommend public class Escape { private int thisCanBeEscape = 0; public Escape(){ new InnerClass(); } private class InnerClass{ public InnerClass (){ log.info("{}",Escape.this.thisCanBeEscape); } } public static void main(String[] args) { new Escape(); } }

執行結果:多線程

15:41:32.697 [main] INFO com.mmall.concurrency.example.publish.Escape - 0

Process finished with exit code 0

分析:app

這個內部類的實例當中,包含了對封裝、實例、隱含和引用,這樣實例在沒有被構造完成以前就會被髮布,可能有不安全的因素在裏面。函數

可能致使this函數引用在構造過程當中逸出的錯誤:

一個線程不管隱式的啓動仍是顯式的啓動,都會形成this引用的逸出;新線程總會在所屬構造完畢以前就看到它過時的值。因此,要在構造函數中構造線程,那麼不要啓用它,而是採用一個start或專有的初始化方法來統一啓動線程,這裏可使用工廠方法和私有構造函數來完成對象建立和監聽器的註冊等等,從而避免不肯定的建立。

發佈不肯定對象會致使的錯誤:

發佈線程之外的全部線程均可以看到被髮布對象的過時的值;

線程看到的被髮布對象的引用是最新的,然而被髮布對象的狀態倒是過時的

注意:若是一個對象是可變對象,那麼它就要安全發佈才能夠。

2、安全發佈對象-四種方法-1

一、安全發佈對象的四種方法:

  •  在靜態初始化函數中初始化一個對象引用;
  •  將對象的引用保存到volatile類型域或者AtomicReference對象中;
  •  將對象的引用保存到某個正確構造對象的final類型域中;
  •  將對象的引用保存到一個由鎖保護的域中。

二、使用代碼演示安全發佈對象的四種方法:

1) 線程不安全的單例-懶漢式

/* * 懶漢模式 * 單例實例在第一次使用的時候進行建立 * */ @NoThreadSafe public class SingletonExample1 { //私有構造函數 private SingletonExample1(){ } //單例對象 private static SingletonExample1 instance = null; //靜態的工廠方法 public static SingletonExample1 getInstance(){ if (instance == null){ instance = new SingletonExample1(); } return instance; } }

注意:懶漢模式是線程不安全的。

2) 餓漢式

/* * 餓漢模式 * 單例實例在類裝載的時候進行建立 * */ @ThreadSafe public class SingletonExample2 { //私有構造函數 private SingletonExample2(){ } //單例對象 private static SingletonExample2 instance = new SingletonExample2(); //靜態的工廠方法 public static SingletonExample2 getInstance(){ return instance; } }

餓漢模式是線程安全的,可是,若是私有構造方法中的操做太多,會形成加載時間過長;同時因爲隨着類裝載,若是沒有使用到該類的實例,會致使資源的浪費。

3)安全的懶漢式:

//靜態的工廠方法 public static synchronized SingletonExample3 getInstance(){ if (instance == null){ instance = new SingletonExample3(); } return instance; }

注意:雖然這麼添加同步鎖後,線程安全了,可是會致使開銷的增長,對性能有很大影響,不推薦這麼操做。

4)使用synchronized對象鎖修飾懶漢模式:

/* * 懶漢模式 ->>雙重同步鎖單例 * 單例實例在第一次使用的時候進行建立 * */ @NoThreadSafe public class SingletonExample4 { //私有構造函數 private SingletonExample4(){ } //單例對象 private static SingletonExample4 instance = null; //靜態的工廠方法 public static SingletonExample4 getInstance(){ if (instance == null){ //雙重檢測機制 synchronized (SingletonExample4.class){ //同步鎖 if(instance == null ){ instance = new SingletonExample4(); } } } return instance; } }

問題:這裏雖然使用了同步鎖機制,可是依然是線程不安全的,爲何呢?

這裏要從CPU的指令分析:

  1. memory = allocate() 分配對象的內存空間
  2. ctorInstance() 初始化對象
  3. Instance = memory 設置instance 指向剛分配的內存

在完成以上三步後,instance就指向了實際分配內存的地址,也就是引用。在單線程狀況下,執行CPU指令操做後,返回對象實例,是沒有任何問題的。

可是,在多線程狀況下,因爲CPU存在指令重排序的問題,指令重排序對單線程是沒有影響的,可是在多線程狀況下就不必定了。

當JVM和CPU優化,發生了指令重排後,上面的執行順序發生了變化:

 1. memory = allocate() 分配對象的內存空間

 2.Instance = memory 設置instance 指向剛分配的內存

 3.ctorInstance() 初始化對象

如上,指令2和指令3發生了重排。

由重排序後的指令順序可知,當線程執行到instance == null的判斷時,此時因爲指令3先執行了,那麼instance就會認爲當前的instance已經實例化過了,而後就會返回instance的實例;然而,此時的指令2並無執行初始化對象,那麼,一旦發生調用instance實例的操做,那麼就會發生錯誤。

3、安全發佈對象-四種方法-2

解決指令重排的問題:

一、使用volatile來修飾變量:

//單例對象 volatile + 雙重檢測機制 - 》 進制指令重排
private volatile static SingletonExample5 instance = null;

volatile能夠制止CPU進行指令重排序的發生。

二、使用靜態域、靜態代碼塊實例化餓漢模式:

/* * 餓漢模式 * 單例實例在類裝載的時候進行建立 * */ @ThreadSafe public class SingletonExample6 { //私有構造函數 private SingletonExample6(){ } //單例對象 private static SingletonExample6 instance = null; static { instance = new SingletonExample6(); } //靜態的工廠方法 public static SingletonExample6 getInstance(){ return instance; } public static void main(String[] args) { System.out.println(getInstance().hashCode()); System.out.println(getInstance().hashCode()); System.out.println(getInstance().hashCode()); System.out.println(getInstance().hashCode()); } }

執行結果:

1450495309
1450495309
1450495309
1450495309

Process finished with exit code 0

注意:當咱們在寫靜態域以及靜態代碼塊時必定要注意他們的順序,順序不一樣,執行的結果也會不一樣。

不一樣的靜態代碼塊是按照順序執行,與普通的方法、代碼塊是不一樣的。

三、使用枚舉實現一個線程安全的單例模式:

/** * 枚舉模式:最安全 * */ @ThreadSafe @Recommend public class SingletonExample7 { // 私有構造函數 private SingletonExample7() { } public static SingletonExample7 getInstance() { return Singleton.INSTANCE.getInstance(); } private enum Singleton { INSTANCE; private SingletonExample7 singleton; // JVM保證這個方法絕對只調用一次 Singleton() { singleton = new SingletonExample7(); } public SingletonExample7 getInstance() { return singleton; } } }

枚舉實現的單例比懶漢式線程要安全的多,同時,因爲JVM的特性,在調用的時候才執行,也只執行一次,避免了重複執行形成的安全問題。避免了資源的浪費。

小結:

  安全發佈對象 - 發佈、逸出

  安全發佈的四中方法 : 餓漢式、volatile、枚舉

相關文章
相關標籤/搜索