什麼是併發編程的原子性、可見性、有序性

摘要

併發程序正確地執行,必須要保證原子性、可見性以及有序性.只要有一個沒有被保證,就有可能會導致程序運行不正確.

  1. 原子性:一個操作或多個操作要麼全部執行完成且執行過程不被中斷,要麼就不執行。

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

  3. 有序性:程序執行的順序按照代碼的先後順序執行。

對於單線程,在執行代碼時jvm會進行指令重排序,處理器爲了提高效率,可以對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證保存最終執行結果和代碼順序執行的結果是一致的。

1.原子性

      何謂原子性操作,即爲最小的操作單元,比如i=1,就是一個原子性操作,這個過程只涉及一個賦值操作。又如i++就不是一個原子操作,它相當於語句i=i+1;這裏包括讀取i,i+1,結果寫入內存三個操作單元。因此如果操作不符合原子性操作,那麼整個語句的執行就會出現混亂,導致出現錯誤的結果,從而導致線程安全問題。因此,在多線程中需要保證線程安全問題,就應該保證操作的原子性,那麼如何保證操作的原子性呢?其一當然是加鎖,這可以保證線程的原子性,比如使用synchronized代碼塊保證線程的同步,從而保證多線程的原子性。但是加鎖的話,就會使開銷比較大。另外,可以使用J.U.C下的atomic來實現原子操作。接下來我們就通過對比來說明atomic實現原子操作的功能。

atomic保證原子性,因爲synchronized加鎖開銷太大


      原子性是指一個操作是不可中斷的,要麼全部執行成功要麼全部執行失敗,有着“同生共死”的感覺。即使在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程所幹擾。

java內存模型中定義了8中操作都是原子的,不可再分的。

  1. lock(鎖定):作用於主內存中的變量,它把一個變量標識爲一個線程獨佔的狀態;
  2. unlock(解鎖):作用於主內存中的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定
  3. read(讀取):作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便後面的load動作使用;
  4. load(載入):作用於工作內存中的變量,它把read操作從主內存中得到的變量值放入工作內存中的變量副本
  5. use(使用):作用於工作內存中的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作;
  6. assign(賦值):作用於工作內存中的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作;
  7. store(存儲):作用於工作內存的變量,它把工作內存中一個變量的值傳送給主內存中以便隨後的write操作使用;
  8. write(操作):作用於主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中。

反應到java代碼中就是---synchronized關鍵字,也就是說synchronized滿足原子性

public class VolatileExample {
    private static volatile int counter = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++)
                        counter++;
                }
            });
            thread.start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

開啓10個線程,每個線程都自加10000次,如果不出現線程安全的問題最終的結果應該就是:10*10000 = 100000;可是運行多次都是小於100000的結果,問題在於 volatile並不能保證原子性,在前面說過counter++這並不是一個原子操作,包含了三個步驟:1.讀取變量counter的值;2.對counter加一;3.將新值賦值給變量counter。如果線程A讀取counter到工作內存後,其他線程對這個值已經做了自增操作後,那麼線程A的這個值自然而然就是一個過期的值,因此,總結果必然會是小於100000的。

如果讓volatile保證原子性,必須符合以下兩條規則:

  1. 運算結果並不依賴於變量的當前值,或者能夠確保只有一個線程修改變量的值;
  2. 變量不需要與其他的狀態變量共同參與不變約束

2. 有序性

synchronized

synchronized語義表示鎖在同一時刻只能由一個線程進行獲取,當鎖被佔用後,其他線程只能等待。因此,synchronized語義就要求線程在訪問讀寫共享變量時只能“串行”執行,因此synchronized具有有序性

volatile包含禁止指令重排序的語義,其具有有序性

3. 可見性

可見性是指當一個線程修改了共享變量後,其他線程能夠立即得知這個修改。通過之前對synchronzed內存語義進行了分析,當線程獲取鎖時會從主內存中獲取共享變量的最新值,釋放鎖的時候會將共享變量同步到主內存中。從而,synchronized具有可見性。同樣的在volatile分析中,會通過在指令中添加lock指令,以實現內存可見性。因此, volatile具有可見性

4. 總結

通過這篇文章,主要是比較了synchronized和volatile在三條性質:原子性,可見性,以及有序性的情況,歸納如下:

synchronized: 具有原子性,有序性和可見性
volatile:具有有序性和可見性

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章