多線程與高併發(二)線程安全

上一篇學習了多線程的一些基礎知識:多線程的基本概念,及建立和操做多線程。內容相對簡單,但多線程的知識確定不會這麼簡單,不然咱們也不須要花這麼多心思去學習,由於多線程中容易出現線程安全問題。java

那麼什麼是線程安全呢,定義以下:程序員

當多個線程訪問同一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替運行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以獲取正確的結果,那這個對象是線程安全的。編程

簡單的理解就是在多線程狀況下代碼的運行結果與預期的正確結果不一致,而產生線程安全的問題通常是由是主內存和工做內存數據不一致性重排序致使的。緩存

要理解這些的必須先理解java的內存模型。安全

一 Java內存模型

在併發編程領域,有兩個關鍵問題:線程之間的通訊同步多線程

1.1 通訊與同步

線程通訊是指線程之間以何種機制來交換信息,在命令式編程中,線程之間的通訊機制有兩種共享內存消息傳遞併發

在共享內存的併發模型裏,線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊,典型的共享內存通訊方式就是經過共享對象進行通訊。app

在消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須經過明確的發送消息來顯式進行通訊,在java中典型的消息傳遞方式就是wait()和notify()。性能

線程同步是指程序用於控制不一樣線程之間操做發生相對順序的機制。在共享內存併發模型裏,同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。在消息傳遞的併發模型裏,因爲消息的發送必須在消息的接收以前,所以同步是隱式進行的。學習

java內存模型是共享內存的併發模型,線程之間主要經過讀-寫共享變量來完成隱式通訊。若是不能理解Java的共享內存模型在編寫併發程序時必定會遇到各類各樣關於內存可見性的問題。

1.2 java內存模型(JMM)

CPU的處理速度和主存的讀寫速度不是一個量級的,爲了平衡這種巨大的差距,每一個CPU都會有緩存。所以,共享變量會先放在主存中,每一個線程都有屬於本身的工做內存,而且會把位於主存中的共享變量拷貝到本身的工做內存,以後的讀寫操做均使用位於工做內存的變量副本,並在某個時刻將工做內存的變量副本寫回到主存中去。JMM就從抽象層次定義了這種方式,而且JMM決定了一個線程對共享變量的寫入什麼時候對其餘線程是可見的。

如圖爲JMM抽象示意圖,線程A和線程B之間要完成通訊的話,要經歷以下兩步:

  1. 線程A從主內存中將共享變量讀入線程A的工做內存後並進行操做,以後將數據從新寫回到主內存中;

  2. 線程B從主存中讀取最新的共享變量

從橫向去看看,線程A和線程B就好像經過共享變量在進行隱式通訊。這其中有頗有意思的問題,若是線程A更新後數據並無及時寫回到主存,而此時線程B讀到的是過時的數據,這就出現了「髒讀」現象。能夠經過同步機制(控制不一樣線程間操做發生的相對順序)來解決或者經過volatile關鍵字使得每次volatile變量都可以強制刷新到主存,從而對每一個線程都是可見的。

java的內存模型內容還有不少,推薦看這篇文章:http://www.javashuo.com/article/p-ezyduzdz-w.html

1.3 可見性和競爭現象

當對象和變量存儲到計算機的各個內存區域時,必然會面臨一些問題,其中最主要的兩個問題是:

  1. 共享對象對各個線程的可見性

  2. 共享對象的競爭現象

共享對象的可見性

當多個線程同時操做同一個共享對象時,若是沒有合理的使用volatile和synchronization關鍵字,一個線程對共享對象的更新有可能致使其它線程不可見。

一個CPU中的線程讀取主存數據到CPU緩存,而後對共享對象作了更改,但CPU緩存中的更改後的對象尚未flush到主存,此時線程對共享對象的更改對其它CPU中的線程是不可見的。最終就是每一個線程最終都會拷貝共享對象,並且拷貝的對象位於不一樣的CPU緩存中。

要解決共享對象可見性這個問題,咱們可使用volatile關鍵字,volatile 關鍵字能夠保證變量會直接從主存讀取,而對變量的更新也會直接寫到主存,這個後面會詳講。

競爭現象

若是多個線程共享一個對象,若是它們同時修改這個共享對象,這就產生了競爭現象。

線程A和線程B共享一個對象obj。假設線程A從主存讀取Obj.count變量到本身的CPU緩存,同時,線程B也讀取了Obj.count變量到它的CPU緩存,而且這兩個線程都對Obj.count作了加1操做。此時,Obj.count加1操做被執行了兩次,不過都在不一樣的CPU緩存中。

要解決競爭現象咱們可使用synchronized代碼塊。synchronized代碼塊能夠保證同一個時刻只能有一個線程進入代碼競爭區,synchronized代碼塊也能保證代碼塊中全部變量都將會從主存中讀,當線程退出代碼塊時,對全部變量的更新將會flush到主存,無論這些變量是否是volatile類型的。

二 重排序

指令重排序是指編譯器和處理器爲了提升性能對指令進行從新排序,重排序通常有如下三種:

  1. 編譯器優化重排序:編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。

  2. 指令級並行的重排序:若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。

  3. 內存系統的重排序:處理器使用緩存和讀寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。

1屬於編譯器重排序,而2和3統稱爲處理器重排序。這些重排序會致使線程安全的問題,JMM確保在不一樣的編譯器和不一樣的處理器平臺之上,經過插入特定類型的Memory Barrier來禁止特定類型的編譯器重排序和處理器重排序,爲上層提供一致的內存可見性保證。

那麼什麼狀況下必定不會重排序呢?編譯器和處理器不會改變存在數據依賴性關係的兩個操做的執行順序,即不會重排序,這裏有個數據依賴性概念是什麼意思呢?看以下代碼:

int a = 1;//A
int b = 2;//B
int c = a + b;//c

這段代碼中A和B沒有任何關係,改變A和B的執行順序,不會對結果產生影響,這裏就能夠對A和B進行指令重排序,由於不論是先執行A或者B都對結果沒有影響,這個時候就說這兩個操做不存在數據依賴性,數據依賴性是指若是兩個操做訪問同一個變量,且這兩個操做有一個爲寫操做,此時這兩個操做就存在數據依賴性,若是咱們對變量a進行了寫操做,後又進行了讀取操做,那麼這兩個操做就是有數據依賴性,這個時候就不能進行指令重排序,這個很好理解,由於若是重排序的話會影響結果。

這裏還有一個概念要理解:as-if-serial:無論怎麼重排序,單線程下的執行結果不能被改變。編譯器、runtime和處理器都必須遵照as-if-serial語義。

這裏也比較好理解,就是在單線程狀況下,重排序不能影響執行結果,這樣程序員沒必要擔憂單線程中重排序的問題干擾他們,也無需擔憂內存可見性問題。

三 happens-before規則

咱們知道處理器和編譯器會對指令進行重排序,可是若是要咱們去了解底層的規則,那對咱們來講負擔太大了,所以,JMM爲程序員在上層提供了規則,這樣咱們就能夠根據規則去推論跨線程的內存可見性問題,而不用再去理解底層重排序的規則。

3.1 happens-before

咱們沒法就全部場景來規定某個線程修改的變量什麼時候對其餘線程可見,可是咱們能夠指定某些規則,這規則就是happens-before。

在JMM中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在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容許這種重排序)。

3.2 具體規則

具體的規則有8條:

具體的一共有六項規則:

  1. 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。

  2. 鎖定規則:一個unLock操做先行發生於後面對同一個鎖額lock操做。

  3. volatile變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做。

  4. 傳遞規則:若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C。

  5. 線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個一個動做。

  6. 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。

  7. 線程終結規則:線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行。

  8. 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始。

參考文章:

http://www.javashuo.com/article/p-ezyduzdz-w.html

相關文章
相關標籤/搜索