併發編程須要處理兩個關鍵問題:線程之間如何通訊以及線程之間如何同步。java
通訊是指線程之間以何種機制來交換信息。線程之間的通訊機制有兩種:共享內存和消息傳遞。git
共享內存模型中,線程之間共享程序的公共狀態,經過讀-寫內存中的公共狀態進行隱式通訊。多條線程共享一片內存,發送者將消息寫入內存,接收者從內存中讀取消息,從而實現了消息的傳遞。程序員
消息傳遞模型中,線程之間經過發送消息來進行顯式通訊。github
同步是指程序中用於控制不一樣線程間操做發生相對順序的機制。在共享內存模型中,須要進行顯式的同步,程序員必須顯式指定某段代碼須要在線程之間互斥執行;在消息傳遞模型中,消息發送必須在消息接收以前,所以同步是隱式進行的。編程
Java採用的是共享內存模型。數組
在 Java 中,全部實例域、靜態域和數組元素存放在堆內存,堆內存在線程之間共享。局部變量、方法定義參數和異常處理器參數不會在線程之間共享。緩存
Java 線程之間的通訊由 Java 內存模型控制,JMM 決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。線程之間的共享變量存儲在主內存中,每一個線程都有一個私有的本地內存,本地內存中存儲了該線程以讀/寫共享變量的副本。 當線程A與線程B之間要通訊的話,首先線程A將本地內存中更新過的共享變量刷新到主內存;而後線程B到主內存去讀取線程A以前已經更新過的共享變量。Java 內存模型和硬件的內存架構不一致,是交叉關係。不管是堆仍是棧,大部分數據都會存儲到內存中,一部分棧和堆的數據也有可能存到CPU寄存器中。Java內存模型試圖屏蔽各類硬件和操做系統的內存訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。安全
Java 內存模型的三大特性:原子性、可見性和順序性多線程
原子性就是指一個操做中要麼所有執行成功,不然失敗。Java內存模型容許虛擬機將沒有被volatile修飾的64位數據(long,double)的讀寫操做劃分爲兩次32位操做進行。架構
i++這樣的操做,實際上是分爲獲取i,i自增以及賦值給i三步的,若是要實現這樣的原子操做就須要使用原子類實現,或者也可使用synchronized互斥鎖來保證操做的原子性。
CAS 也就是 CompareAndSet, 在Java中能夠經過循環CAS來實現原子操做。在JVM內部,除了偏向鎖,JVM實現鎖的方式都是用了CAS,也就是當一個線程想進入同步塊的時候使用CAS獲取鎖,退出時使用CAS釋放鎖。
可見性指的是當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值來實現可見性的。
執行程序時,爲了提升性能,編譯器和處理器經常會對指令進行重排序。
重排序可能致使多線程程序出現內存可見性問題。JMM 經過插入特定類型的內存屏障指令來禁止特定類型的處理器重排序,確保了不一樣的編譯器和處理器平臺上,能提供一致的內存可見性保證。
若是兩個操做訪問同一個變量,且這兩個操做中有一個是寫操做,這兩個操做之間就存在數據依賴性。在重排序時,會遵照數據依賴性,不會改變存在數據依賴關係的兩個操做的執行順序,也就是不會重排序。可是,這是針對單個處理器或單個線程而言的,多線程或多處理器之間的數據依賴性不被考慮在內。
無論怎麼重排序,單線程程序的執行結果不能被改變。as-if-serial 語義使得單線程程序員無需擔憂重排序的干擾。
重排序可能會改變多線程程序的執行結果,以下圖所示
JMM 一方面要爲程序員提供足夠強的內存可見性保證;另外一方面,對編譯器和處理器的限制要儘量放鬆。
JMM 對不一樣性質的重排序,採起了不一樣的策略:
JSR-133 中對 happens-before 關係定義以下:
happens-before 與 as-if-serial 相比,後者保證了單線程內程序的執行結果不被改變;前者保證正確同步的多線程程序的執行結果不被改變。
JSR-133中定義了以下的 happens-before 規則:
可見性有三種實現方式:
在一個線程中寫一個變量,在另外一個線程中讀一個變量,並且寫和讀沒有經過同步來排序。
在理想化的順序一致性內存模型中,有兩大特性:
JMM對正確同步的多線程程序的內存一致性作了以下保證:若是程序是正確同步的,程序的執行將具備順序一致性,也即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。
JMM 的實現方針爲:在不改變正確同步的程序執行結果的前提下,儘量爲優化提供方便。所以,JMM 與上述理想化的順序一致性內存模型有以下差別:
Java中可使用volatile關鍵字來保證順序性,還能夠用synchronized和lock來保證。
volatile 關鍵字解決的是內存可見性的問題,會使得全部對 volatile 變量的讀寫都會直接刷新到主存,保證了變量的可見性。
要注意的是,使用 volatile 關鍵字僅能實現對原始變量操做的原子性(boolean,int,long等),不能保證符合操做的原子性(如i++)。
一個 volatile 變量的單個讀/寫操做,和使用同一個鎖對普通變量的讀/寫操做進行同步,執行的效果是相同的。鎖的 happens-before 規則保證釋放鎖和獲取鎖的兩個線程之間的內存可見性,這意味着對一個 volatile 變量的讀,總能看到對這個變量最後的寫入,從而實現了可見性。須要注意的是,對任意單個 volatile 變量的讀/寫具備原子性,可是相似於i++這種複合操做不具備原子性。
當寫一個 volatile 變量時,JMM 會把該線程對應的本地內存中的共享變量值刷新到內存。 當讀一個 volatile 變量時,JMM 會把線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。
具體來講,線程A寫一個 volatile 變量,實質上是線程A向接下來將要讀這個 volatile 變量的線程發出了它修改的信息;線程B讀一個 volatile 變量,實質上是線程B接收了以前某個線程發出的修改信息。
JVM 是經過進入和退出對象監視器來實現同步的。Java 中的每個對象均可以做爲鎖。
JDK 1.6 中對 synchronized 進行了優化,爲了減小獲取和釋放鎖帶來的消耗引入了偏向所和輕量鎖。也就是說鎖一共有四種狀態,級別從低到高分別是:無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態。鎖能夠升級可是不能降級。
synchronized 使用的鎖是存放在 Java 對象頭中的。若是對象是數組類型,則虛擬機用3個Word(字寬)存儲對象頭,若是對象是非數組類型,則用2字寬存儲對象頭。
Java 頭中包含了Mark Word,用來存儲對象的 hashCode 或者鎖信息,在運行期間其中存儲的數據會隨着鎖的標誌位的變化而變化。
大多數狀況下,鎖不只不存在多線程競爭,並且老是由統一線程屢次得到,爲了讓線程獲取鎖的代價更低而引入了偏向鎖。
它的核心思想是:若是一個線程得到了鎖,那麼鎖就進入偏向模式。當這個線程再次請求鎖時,無須再作任何同步操做。這樣就節省了大量有關鎖申請的操做,從而提升了程序性能。所以,對於幾乎沒有鎖競爭的場合,偏向鎖有比較好的優化效果,由於連續屢次極有多是同一個線程請求相同的鎖。而對於鎖競爭比較激烈的場合,其效果不佳。
釋放鎖:當有另一個線程獲取這個鎖時,持有偏向鎖的線程就會釋放鎖,釋放時會等待全局安全點(這一時刻沒有字節碼運行),接着會暫停擁有偏向鎖的線程,根據鎖對象目前是否被鎖來斷定將對象頭中的 Mark Word 設置爲無鎖或者是輕量鎖狀態。
加鎖: 當代碼進入同步塊時,若是同步對象爲無鎖狀態時,當前線程會在棧幀中建立一個鎖記錄(Lock Record)區域,同時將鎖對象的對象頭中 Mark Word 拷貝到鎖記錄中,再嘗試使用 CAS 將 Mark Word 更新爲指向鎖記錄的指針。若是更新成功,當前線程就得到了鎖。若是更新失敗 JVM 會先檢查鎖對象的 Mark Word 是否指向當前線程的鎖記錄。若是是則說明當前線程擁有鎖對象的鎖,能夠直接進入同步塊。不是則說明有其餘線程搶佔了鎖,嘗試使用自旋鎖來獲取鎖。
**解鎖:**輕量鎖的解鎖過程也是利用 CAS 來實現的,會嘗試鎖記錄替換回鎖對象的 Mark Word 。若是替換成功則說明整個同步操做完成,失敗則說明有其餘線程嘗試獲取鎖,這時就會喚醒被掛起的線程(此時已經膨脹爲重量鎖)
鎖類型 | 優勢 | 缺點 | 使用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不須要額外的消耗,和執行非同步方法比僅存在納秒級的差距 | 若是線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用於只有一個線程訪問同步塊場景 |
輕量級鎖 | 競爭的線程不會阻塞,提升了程序的響應速度 | 若是始終得不到鎖競爭的線程使用自旋會消耗CPU | 追求響應時間,鎖佔用時間很短 |
重量級鎖 | 線程競爭不使用自旋,不會消耗CPU | 線程阻塞,響應時間緩慢 | 追求吞吐量,鎖佔用時間較長 |
釋放鎖與volatile寫有相同的內存語義,線程A釋放鎖,是A向要獲取鎖的線程發出A對共享變量修改的消息。 獲取鎖與volatile讀有相同的內存語義,是線程B接收了以前線程發出的堆共享變量作的修改的消息。
從 ReentrantLock 中能夠看到:
在juc包中源代碼實現,能夠發現Java線程之間通訊的通用化實現模式:
對於 final 域,遵循兩個重排序規則:
public class FinalExample{
int i;
final int j;
static FinalExample obj;
public FinalExample(){
i=1;
j=2;
}
public static void writer(){
obj=new FinalExample();
}
public static void reader(){
FinalExample object=obj;
int a=object.i;
int b=object.j;
}
}
複製代碼
假設線程A執行 writer() 方法,線程B執行 reader() 方法。
寫final域的重排序規則 寫 final 域的重排序規則禁止把 final 域的寫重排序到構造函數以外。從而確保了在對象引用被任意線程可見以前,對象的final域已經被正確的初始化過了。在上述的代碼中,線程B得到的對象,final域必定被正確初始化,普通域i卻不必定。
讀final域的重排序規則 在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序該操做。從而確保在讀一個對象的final域以前,必定會先讀包含這個final域的對象的引用
final域爲引用類型 在構造函數內對一個final引用的對象的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變量,不能重排序。
可是,要獲得上述的效果,須要保證在構造函數內部,不能讓這個被構造對象的引用被其餘線程所見,也就是不能有this逸出。
https://juejin.im/post/5c122d00e51d4541284cc592