本人免費整理了Java高級資料,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分佈式等教程,一共30G,須要本身領取。
傳送門:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q
在上一篇文章中總結了java
線程的狀態轉換以及基本操做node
對多線程已經有一點基本的認識了,若是多線程編程只有這麼簡單,那咱們就沒必要費勁周折的去學習它了。在多線程中稍微不注意就會出現線程安全問題,那麼什麼是線程安全問題?個人認識是,在多線程下代碼執行的結果與預期正確的結果不一致,該代碼就是線程不安全的,不然則是線程安全的。程序員
雖然這種回答彷佛不能獲取什麼內容,能夠google下。在<<深刻理解Java虛擬機>>中看到的定義。原文以下: 當多個線程訪問同一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替運行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以獲取正確的結果,那這個對象是線程安全的。編程
關於定義的理解這是一個仁者見仁智者見智的事情。出現線程安全的問題通常是由於主內存和工做內存數據不一致性和重排序致使的,而解決線程安全的問題最重要的就是理解這兩種問題是怎麼來的,那麼,理解它們的核心在於理解java內存模型(JMM)。數組
在多線程條件下,多個線程確定會相互協做完成一件事情,通常來講就會涉及到多個線程間相互通訊告知彼此的狀態以及當前的執行結果等,另外,爲了性能優化,還會涉及到編譯器指令重排序和處理器指令重排序。下面會一一來聊聊這些知識。緩存
線程間協做通訊能夠類比人與人之間的協做的方式,在現實生活中,以前網上有個流行語「你媽喊你回家吃飯了」,就以這個生活場景爲例,小明在外面玩耍,小明媽媽在家裏作飯,作晚飯後準備叫小明回家吃飯,那麼就存在兩種方式:安全
小明媽媽要去上班了十分緊急這個時候手機又沒有電了,因而就在桌子上貼了一張紙條「飯作好了,放在...」小明回家後看到紙條如願吃到媽媽作的飯菜,那麼,若是將小明媽媽和小明做爲兩個線程,那麼這張紙條就是這兩個線程間通訊的共享變量,經過讀寫共享變量實現兩個線程間協做;性能優化
還有一種方式就是,媽媽的手機還有電,媽媽在趕去坐公交的路上給小明打了個電話,這種方式就是通知機制來完成協做。一樣,能夠引伸到線程間通訊機制。多線程
經過上面這個例子,應該有些認識。在併發編程中主要須要解決兩個問題:1. 線程之間如何通訊;2.線程之間如何完成同步(這裏的線程指的是併發執行的活動實體)。通訊是指線程之間以何種機制來交換信息,主要有兩種:共享內存和消息傳遞。這裏,能夠分別類比上面的兩個舉例。併發
java內存模型是共享內存的併發模型,線程之間主要經過讀-寫共享變量來完成隱式通訊。若是程序員不能理解Java的共享內存模型在編寫併發程序時必定會遇到各類各樣關於內存可見性的問題。
1.哪些是共享變量
在java程序中全部實例域,靜態域和數組元素都是放在堆內存中(全部線程都可訪問到,是能夠共享的),而局部變量,方法定義參數和異常處理器參數不會在線程間共享。共享數據會出現線程安全的問題,而非共享數據不會出現線程安全的問題。關於JVM運行時內存區域在後面的文章會講到。
2.JMM抽象結構模型
咱們知道CPU的處理速度和主存的讀寫速度不是一個量級的,爲了平衡這種巨大的差距,每一個CPU都會有緩存。所以,共享變量會先放在主存中,每一個線程都有屬於本身的工做內存,而且會把位於主存中的共享變量拷貝到本身的工做內存,以後的讀寫操做均使用位於工做內存的變量副本,並在某個時刻將工做內存的變量副本寫回到主存中去。JMM就從抽象層次定義了這種方式,而且JMM決定了一個線程對共享變量的寫入什麼時候對其餘線程是可見的。
如圖爲JMM抽象示意圖,線程A和線程B之間要完成通訊的話,要經歷以下兩步:
從橫向去看看,線程A和線程B就好像經過共享變量在進行隱式通訊。這其中有頗有意思的問題,若是線程A更新後數據並無及時寫回到主存,而此時線程B讀到的是過時的數據,這就出現了「髒讀」現象。能夠經過同步機制(控制不一樣線程間操做發生的相對順序)來解決或者經過volatile關鍵字使得每次volatile變量都可以強制刷新到主存,從而對每一個線程都是可見的。
一個好的內存模型實際上會放鬆對處理器和編譯器規則的束縛,也就是說軟件技術和硬件技術都爲同一個目標而進行奮鬥:在不改變程序執行結果的前提下,儘量提升並行度。JMM對底層儘可能減小約束,使其可以發揮自身優點。所以,在執行程序時,爲了提升性能,編譯器和處理器經常會對指令進行重排序。通常重排序能夠分爲以下三種:
如圖,1屬於編譯器重排序,而2和3統稱爲處理器重排序。這些重排序會致使線程安全的問題,一個很經典的例子就是DCL問題,這個在之後的文章中會具體去聊。針對編譯器重排序,JMM的編譯器重排序規則會禁止一些特定類型的編譯器重排序;針對處理器重排序,編譯器在生成指令序列的時候會經過插入內存屏障指令來禁止某些特殊的處理器重排序。
那麼什麼狀況下,不能進行重排序了?下面就來講說數據依賴性。
有以下代碼:
double pi = 3.14 //A double r = 1.0 //B double area = pi * r * r //C
這是一個計算圓面積的代碼,因爲A,B之間沒有任何關係,對最終結果也不會存在關係,它們之間執行順序能夠重排序。所以能夠執行順序能夠是A->B->C或者B->A->C執行最終結果都是3.14,即A和B之間沒有數據依賴性。
具體的定義爲:若是兩個操做訪問同一個變量,且這兩個操做有一個爲寫操做,此時這兩個操做就存在數據依賴性這裏就存在三種狀況:1. 讀後寫;2.寫後寫;3. 寫後讀,者三種操做都是存在數據依賴性的,若是重排序會對最終執行結果會存在影響。
編譯器和處理器在重排序時,會遵照數據依賴性,編譯器和處理器不會改變存在數據依賴性關係的兩個操做的執行順序
另外,還有一個比較有意思的就是as-if-serial語義。
as-if-serial
as-if-serial語義的意思是:無論怎麼重排序(編譯器和處理器爲了提供並行度),(單線程)程序的執行結果不能被改變。編譯器,runtime和處理器都必須遵照as-if-serial語義。as-if-serial語義把單線程程序保護了起來,遵照as-if-serial語義的編譯器,runtime和處理器共同爲編寫單線程程序的程序員建立了一個幻覺:單線程程序是按程序的順序來執行的。
好比上面計算圓面積的代碼,在單線程中,會讓人感受代碼是一行一行順序執行上,實際上A,B兩行不存在數據依賴性可能會進行重排序,即A,B不是順序執行的。as-if-serial語義使程序員沒必要擔憂單線程中重排序的問題干擾他們,也無需擔憂內存可見性問題。
4. happens-before規則
上面的內容講述了重排序原則,一會是編譯器重排序一會是處理器重排序,若是讓程序員再去了解這些底層的實現以及具體規則,那麼程序員的負擔就過重了,嚴重影響了併發編程的效率。所以,JMM爲程序員在上層提供了六條規則,這樣咱們就能夠根據規則去推論跨線程的內存可見性問題,而不用再去理解底層重排序的規則。下面以兩個方面來講。
4.1 happens-before定義
happens-before的概念最初由Leslie Lamport在其一篇影響深遠的論文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出,有興趣的能夠google一下。JSR-133使用happens-before的概念來指定兩個操做之間的執行順序。因爲這兩個操做能夠在一個線程以內,也能夠是在不一樣線程之間。
所以,JMM能夠經過happens-before關係向程序員提供跨線程的內存可見性保證(若是A線程的寫操做a與B線程的讀操做b之間存在happens-before關係,儘管a操做和b操做在不一樣的線程中執行,但JMM向程序員保證a操做將對b操做可見)。
具體的定義爲:
1)若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見,並且第一個操做的執行順序排在第二個操做以前。
2)兩個操做之間存在happens-before關係,並不意味着Java平臺的具體實現必需要按照happens-before關係指定的順序來執行。若是重排序以後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法(也就是說,JMM容許這種重排序)。
上面的1)是JMM對程序員的承諾。從程序員的角度來講,能夠這樣理解happens-before關係:若是A happens-before B,那麼Java內存模型將向程序員保證——A操做的結果將對B可見,且A的執行順序排在B以前。注意,這只是Java內存模型向程序員作出的保證!
上面的2)是JMM對編譯器和處理器重排序的約束原則。正如前面所言,JMM實際上是在遵循一個基本原則:只要不改變程序的執行結果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎麼優化都行。
JMM這麼作的緣由是:程序員對於這兩個操做是否真的被重排序並不關心,程序員關心的是程序執行時的語義不能被改變(即執行結果不能被改變)。所以,happens-before關係本質上和as-if-serial語義是一回事。
下面來比較一下as-if-serial和happens-before:
as-if-serial VS happens-before
4.2 具體規則
具體的一共有六項規則:
下面以一個具體的例子來說下如何使用這些規則進行推論:
依舊以上面計算圓面積的進行描述。利用程序順序規則(規則1)存在三個happens-before關係:
1. A happens-before B;
2. B happens-before C;
3. A happens-before C。
這裏的第三個關係是利用傳遞性進行推論的。
A happens-before B,定義1要求A執行結果對B可見,而且A操做的執行順序在B操做以前,但與此同時利用定義中的第二條,A,B操做彼此不存在數據依賴性,兩個操做的執行順序對最終結果都不會產生影響,在不改變最終結果的前提下,容許A,B兩個操做重排序,即happens-before關係並不表明了最終的執行順序。
5. 總結
上面已經聊了關於JMM的兩個方面:
1. JMM的抽象結構(主內存和線程工做內存);
2. 重排序以及happens-before規則。
接下來,咱們來作一個總結。從兩個方面進行考慮。
1. 若是讓咱們設計JMM應該從哪些方面考慮,也就是說JMM承擔哪些功能;
2. happens-before與JMM的關係;
3. 因爲JMM,多線程狀況下可能會出現哪些問題?
5.1 JMM的設計
JMM是語言級的內存模型,在個人理解中JMM處於中間層,包含了兩個方面:
(1)內存模型;
(2)重排序以及happens-before規則。
同時,爲了禁止特定類型的重排序會對編譯器和處理器指令序列加以控制。
而上層會有基於JMM的關鍵字和J.U.C包下的一些具體類用來方便程序員可以迅速高效率的進行併發編程。站在JMM設計者的角度,在設計JMM時須要考慮兩個關鍵因素:
另外還要一個特別有意思的事情就是關於重排序問題,更簡單的說,重排序能夠分爲兩類:
JMM對這兩種不一樣性質的重排序,採起了不一樣的策略,以下。
JMM的設計圖爲:
從圖能夠看出:
5.2 happens-before與JMM的關係
一個happens-before規則對應於一個或多個編譯器和處理器重排序規則。對於Java程序員來講,happens-before規則簡單易懂,它避免Java程序員爲了理解JMM提供的內存可見性保證而去學習複雜的重排序規則以及這些規則的具體實現方法
5.3 從此可能須要關注的問題
從上面內存抽象結構來講,可能出在數據「髒讀」的現象,這就是數據可見性的問題,另外,重排序在多線程中不注意的話也容易存在一些問題,好比一個很經典的問題就是DCL(雙重檢驗鎖),這就是須要禁止重排序,另外,在多線程下原子操做例如i++不加以注意的也容易出現線程安全的問題。但總的來講,在多線程開發時須要從原子性,有序性,可見性三個方面進行考慮。J.U.C包下的併發工具類和併發容器也是須要花時間去掌握的,這些東西在之後得文章中多會一一進行討論。