1. 程式人生 > >多線程學習:Volatile與Synchronized的區別、什麽是重排序

多線程學習:Volatile與Synchronized的區別、什麽是重排序

art 不可 順序 經典的 排序 傳遞 -s style family

java線程的內存模型

技術分享圖片

  java的線程內存模型中定義了每個線程都有一份自己的共享變量副本(本地內存),裏面存放自己私有的數據,其他線程不能直接訪問,而一些共享變量則存在主內存中,供所有線程訪問。
上圖中,如果線程A和線程B要進行通信,就要經過主內存,比如線程B要獲取線程A修改後的共享變量的值,要經過下面兩步:

(1)、線程A修改自己的共享變量副本,並刷新到了主內存中。
(2)、線程B讀取主內存中被A更新過的共享變量的值,同步到自己的共享變量副本中。

總結:在java內存模型中,共享變量存放在主內存中,每個線程都有自己的本地內存,當多個線程同時訪問一個數據的時候,可能本地內存沒有及時刷新到主內存,所以就會發生線程安全問題。

java多線程中的三個特性:

  原子性:即一個操作或者多個操作 要麽全部執行並且執行的過程不會被任何因素打斷,要麽就都不執行。一個很經典的例子就是銀行賬戶轉賬問題:比如從賬戶A向賬戶B轉1000元,那麽必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。這2個操作必須要具備原子性才能保證不出現一些意外的問題。

  可見性:當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

  有序性:就是程序執行的順序按照代碼的先後順序執行。一般來說處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。如下:

      int a = 10; //語句1

      int r = 2; //語句2

      a = a + 3; //語句3

      r = a*a; //語句4

因為重排序,他還可能執行順序為 2-1-3-4,1-3-2-4。但絕不可能 2-1-4-3,因為這打破了依賴關系。顯然重排序對單線程運行是不會有任何問題,而多線程就不一定了,所以我們在多線程編程時就得考慮這個問題了。


Volatile關鍵字的作用:

  其實volatile關鍵字的作用就是保證了可見性和有序性(不保證原子性),如果一個共享變量被volatile關鍵字修飾,那麽如果一個線程修改了這個共享變量後,其他線程是立馬可知的。如果線程A修改了自己的共享變量副本,這時如果該共享變量沒有被volatile修飾,那麽本次修改不一定會馬上將修改結果刷新到主存中,如果此時B去主存中讀取共享變量的值,那麽這個值就是沒有被A修改之前的值。如果該共享變量被volatile修飾了,那麽本次修改結果會強制立刻刷新到主存中,如果此時B去主存中讀取共享變量的值,那麽這個值就是被A修改之後的值了。


  volatile禁止指令重排序優化,在指令重排序優化時,在volatile變量之前的指令不能在volatile之後執行,在volatile之後的指令也不能在volatile之前執行,所以它保證了有序性。

  volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因為它需要在本地代碼中插入許多內存屏障指令(是一種CPU指令,用於控制特定條件下的重排序和內存可見性問題。Java編譯器也會根據內存屏障的規則禁止重排序。)來保證處理器不發生亂序執行。

synchronized關鍵字的作用:  

  synchronized提供了同步鎖的概念,被synchronized修飾的代碼段可以防止被多個線程同時執行,必須一個線程把synchronized修飾的代碼段都執行完畢了,其他的線程才能開始執行這段代碼。 因為synchronized保證了在同一時刻,只能有一個線程執行同步代碼塊,所以執行同步代碼塊的時候相當於是單線程操作了,那麽線程的可見性、原子性、有序性(線程之間的執行順序)它都能保證了。synchronized並沒有禁止重排序,但是synchronized相當於是一個單線程了,所以有沒有重排序對程序都是沒有影響的。

Volatile和synchronized的區別: 

  (1)、volatile只能作用於變量,使用範圍較小。synchronized可以用在變量、方法、類、同步代碼塊等,使用範圍比較廣。
  (2)、volatile只能保證可見性和有序性,不能保證原子性。而可見性、有序性、原子性synchronized都可以包證。
  (3)、volatile不會造成線程阻塞。synchronized可能會造成線程阻塞。
  (4)、在性能方面synchronized關鍵字是防止多個線程同時執行一段代碼,就會影響程序執行效率,而volatile關鍵字在某些情況下性能要優於synchronized。

什麽是重排序:

  重排序是指編譯器和處理器為了優化程序性能而對指令序列進行重新排序的一種手段。但是重排序可以保證最終執行的結果是與程序順序執行的結果一致,並且只會對不存在數據依賴性的指令進行重排序,這個重排序在單線程下對最終執行結果是沒有影響的,但是在多線程下就會存在問題。

可以看一個例子 

class ReorderExample {
 int a = 0;
boolean flag = false;

// 寫入線程
public void writer() {
a = 1; // 1
flag = true; // 2

}// 讀取的線程
public void reader() {
if (flag) { // 3
int i = a * a; // 4 

}
}
}

如上面代碼,如果兩個線程同時執行在沒有發生重排序的時候int i =1,如果發生了重排序那麽1,2的位置因為不存在數據依賴可以會發生位置的互換。那麽這時候int i =0;當然這個在單線程是沒有問題的。只有在多線程才會發生這種情況

    volatile int a = 0;
    volatile boolean flag = false;

我們只需要加上volatile關鍵字也是可以避免這種問題的,volatile是禁止重排序的。

什麽是數據依賴?

int a = 1;(1)
int b = 2;(2)
int c= a + b;(3)

這裏面第三步就存在數據依賴。編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序。所以這裏面無論(1)(2)有沒有發生重排序,(3)都是在他們之後執行。這裏所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮。

s-if-serial語義

  無論怎麽排序(編譯器和處理器為了提高並行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵守as-if-serial語義。

  為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關系的操作做重排序。

happens-before語義

  如果一個操作執行的結果需要對另一個操作可見,那麽這兩個操作之間必須要存在happens-before關系。這裏提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。

具體定義:

  1、如果一個操作happens-before另一個操作,那麽第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。

  2、兩個操作之間存在happens-before關系,並不意味著Java平臺的具體實現必須要按照 happens-before關系指定的順序來執行。如果重排序之後的執行結果,與按happens-before關系來執行的結果一致,那麽這種重排序並不非法(如果A happens- before B,JMM並不要求A一定要在B之前執行。JMM僅僅要求前一個操作(執行的結果)對後一個操作可見,且前一個操作按順序排在第二個操作之前。這裏操作A的執行結果不需要對操作B可見;而且重排序操作A和操作B後的執行結果,與操作A和操作B按happens- before順序執行的結果一致。在這種情況下,JMM會認為這種重排序並不非法(not illegal),JMM允許這種重排序)。 

happens-before規則如下:

  程序順序規則:一個線程中的每個操作,happens-before於該線程中的任意後續操作。
  監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
  volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
  傳遞性:如果A happens-before B,且B happens-before C,那麽A happens-before C。
  start()規則:如果線程A執行操作ThreadB.start()(啟動線程B),那麽A線程的ThreadB.start()操作happens-before於線程B中的任意操作。
  join()規則:如果線程A執行操作ThreadB.join()並成功返回,那麽線程B中的任意操作happens-before於線程A從ThreadB.join()操作成功返回。

  

多線程學習:Volatile與Synchronized的區別、什麽是重排序