併發程式設計與執行緒安全

Stream_who發表於2019-10-29

一、基本概念

1. 執行緒安全:當多個執行緒訪問某個類時,不管執行時環境採用何種排程方式或者這些程式將如何交替執行,並且在主調程式碼中不需要任何額外的同步或協調,這個類都能表現出正確的行為,那麼就稱這個類是執行緒安全的
2. 執行緒安全性體現:
1)原子性:提供了互斥訪問,同一時刻只能有一個執行緒來對它進行操作

2)可見性:一個執行緒對主記憶體的修改可以及時的被其他執行緒觀察到

3)有序性:一個執行緒觀察其他執行緒中的指令執行順序,由於指令重排序的存在,該觀察結果一般是雜亂無序

二、原子性

1. Atomic包
1)基本說明

	(1)位於java.util.concurrent.atomic

	(2)引用類:AtomicXXX (底層方法核心,Unsafe.compareAndSwapXXX)

	(3)AtomicReference 所提供的某些方法可以進行原子性操作,如compareAndSet、getAndSet,這僅僅是對引用進行原子性操作(不能保證物件中若存在屬性值修改是執行緒安全的)

2)CAS(Compare and swap,比較並交換)

	(1)該演算法其實是計算機硬體對於併發操作共享資料的支援,保證了資料的原子性

	(2)CAS演算法理解起來非常簡單,CAS包含三個運算元:記憶體值 V;預估值(expect) A;更新值(update) B;

		a. 比較 A 與 V 是否相等。(比較)

		b. 如果比較相等,將 B 寫入 V。(交換)

		c. 返回操作是否成功 
2. 鎖
JAVA中能保證同一時刻,只有一個執行緒來進行對其進行操作的,除了atomic包中所提供的類之外,還有jdk提供的鎖,JAVA主要提供以下鎖:

1)synchronized :關鍵字,並且依賴與JVM,作用物件的作用範圍內都是同一時刻只能有一個執行緒對其操作的

2)Lock : 介面類,依賴特殊的CPU指定,使用程式碼實現,常用子類ReentrantLock
3. 對比
1)synchronized :不可中斷鎖,適合競爭不激烈,可讀性較好

2)Lock:可中斷鎖,多樣化同步,競爭激烈時能維持常態

3)Atomic:競爭激烈時能維持常態,比Lock效能好;只能同步一個值

三、可見性

一個執行緒對主記憶體的修改可以及時的被其他執行緒觀察到

1. 導致共享變數線上程間不可見的原因
1)執行緒交叉執行

2)重排序結合執行緒交叉執行

3)共享變數更新後的值沒有在工作記憶體與主存間及時更新
2. synchronized
 JMM關於synchronized的兩條規定:

1)執行緒解鎖前,必須把共享變數的最新值重新整理到主記憶體

2)執行緒加鎖時,將清空工作記憶體中共享變數的值,從而使用共享變數時需要從主記憶體中重新讀取最新的值(注意:加鎖與解鎖是同一把鎖)
3. volatile(通過加入記憶體屏障和禁止重排序優化來實現)
1)對volatile變數寫操作時,會在寫操作後加入一條store屏障指令,將本地記憶體中的共享變數值重新整理到主記憶體中

2)對volatile變數讀操作是,會在讀操作前加入一條load屏障指令,從主記憶體中讀取共享變數

通過上面兩點,任何時候,不同的執行緒總能看到該變數的最新值。所有的指令操作都是CPU級別的

3)結論

	(1)volatile進行加操作執行緒不安全的,不適合計數場景

	(2)volatile關鍵字不具有原子性

4)使用場景

	使用volatile必須具備兩個條件

	(1)對變數的寫操作,不依賴於當前值

	(2)該變數沒有包含在具有其他變數的不變式子中

	因此volatile適合作為狀態的標記量

四、有序性

JAVA記憶體模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單執行緒程式的執行,卻會影響到多執行緒併發執行的正確性

1. volatile、synchronized、Lock
1)通過 volatile、synchronized、Lock 保證一定的有序性。顯然,synchronized、Lock 保證每一個時刻只有一個執行緒可以執行被同步的程式碼,相當於讓執行緒順序執行同步程式碼,從而保證有序性
2. happens-before原則

JMM具備一些先天的有序性,即不需要額外的手段,就能保證有序性,即 Happens-before 原則,如果兩個操作的執行次序,沒有辦法通過 Happens-before 原則推到出來,虛擬機器進行隨意的重排序,那麼就不能保證有序性

1)程式次序規則:一個執行緒內,按照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作

2)鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作;

3)傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C

4)執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每個一個動作

5)執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生

6)執行緒終結規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行

7)物件終結規則:一個物件的初始化完成先行發生於他的finalize()方法的開始

五、安全釋出物件

多執行緒併發環境下,執行緒安全極為重要。往往一些問題的發生都是由於不正確的釋出了物件造成了物件逸出而引起的,因此如果系統開發中需要釋出一些物件,必須要做到安全釋出,以免造成安全隱患

1. 釋出與逸出
1)釋出物件:使一個物件能夠被當前範圍之外的程式碼所使用

	(1)在我們的日常開發中,經常要釋出一些物件,比如通過類的非私有方法返回物件的引用,或者通過公有靜態變數釋出物件

2)物件逸出:一種錯誤的釋出情況,當一個物件還沒有構造完成時,就使它被其他執行緒所見(在物件未完成構造之前,不可以將其釋出)

	(1)一個導致this引用在構造期間逸出的錯誤,是在建構函式中啟動一個執行緒,無論是隱式啟動執行緒,還是顯式啟動執行緒,都會造成this引用逸出,新執行緒總會在所屬物件構造完畢前看到它

	(2)所以如果要在建構函式中建立執行緒,那麼不要啟動它,而應該採用一個專有的start或initialize方法來統一啟動執行緒。我們可以採用工廠方法和私有建構函式來完成物件建立和監聽器的註冊,這樣就可以避免不正確的建立。記住,我們的目的是,在物件未完成構造之前,不可以將其釋出
2. 安全釋出物件的四種方法
1)在靜態初始化函式中初始化一個物件引用

2)將物件的引用儲存到volatile型別域或者AtomicReference物件中

3)將物件的引用儲存到某個正確構造物件的final型別域中	

4)將物件的引用儲存到一個由鎖保護的域中

參考網址

JAVA併發程式設計與高併發解決方案 - 併發程式設計 二(一系列)

注:文章是經過參考其他的文章然後自己整理出來的,有可能是小部分參考,也有可能是大部分參考,但絕對不是直接轉載,覺得侵權了我會刪,我只是把這個用於自己的筆記,順便整理下知識的同時,能幫到一部分人。
ps : 有錯誤的還望各位大佬指正,小弟不勝感激

相關文章