併發編程併發不可預期結果的根本原因

提出問題

說到併發,我們首先應該給自己提出下面這三個問題:

  1. 產生併發的根本原因是什麼?
  2. 會造成什麼後果?
  3. 怎麼去控制,處理併發達到我們預期的結果。
提出這三個問題之後,我們慢慢來看看一下幾個知識點,這三問題自然迎刃而解。解答上面3個問題之前,我們需要對JVM的內存模型有所瞭解,在《JVM內存模型》一文中已經將JVM內存模型講的很清楚了,對JVM內存模型不瞭解的同學可以先去看看,然後繼續本文。

在線程的角度來說,內存分爲共享內存和私有內存兩個部分。線程訪問一個共享區的資源時,會copy一份到私有內存操作棧中,進行計算處理,處理完成之後會在線程消亡前的某一個時機,將最終的結果刷新到共享內存中。在多線程的情況下,計算機允許多個線程同時運行。這樣就會出現一個問題,緩存不一致性問題。

舉例說明:A,B兩個線程都需要訪問數據data = 0,但是訪問的時機是不可控的。現在A去共享內存區域讀取了data,copy一份到私有內存,開始處理比如說+1操作,處理到一半的時候,B也去共享內存區域讀取了data,也copy一份到了私有內存區域開始處理,因爲A還沒有做完+1操作,還沒有將最新結果刷新到共享內存,所以B讀取到的不是data=1而是0,這時候B也開始做+1操作,得到的結果也是data=1。現在A處理完了,將數據刷新到共享內存,data = 1,B也做完了,將數據刷新到共享內存data=1。最終結果data=1,如果說A,在B讀取之前就已經處理完成,並刷新了共享內存中的數據,那B讀取到的是data=1,那麼結果就是2。最後得出結論:多線程訪問共享內存中數據時,得到的結果是不可控制的。

通過上面的案例,我們解答了上面的1,2兩個問題。1。產生併發的原因是,線程訪問共享內存中的數據,需要copy一份處理完成後在不確定時機刷新共享內存,並且A,B之間更改數據的時候,彼此不知道,還有A,B誰先執行,什麼時候執行都不可控。2。造成的結果就是最終結果不可控,不一定得到我們預期的結果。

下面給出一個具體案例,可以自己運行試試看結果:

線程:

/**
 * Created by PICO-USER on 2017/11/9.
 */

public class AdditionRun implements Runnable {
    public int count = 0;

    @Override
    public void run() {
        try {
            //休眠10毫秒,模擬耗時操作,以便等待其他線程也啓動起來了
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count += 1;
    }
}

程序入口:

public class MyClass {

    public static void main(String[] args0) throws InterruptedException {

        AdditionRun additionRun = new AdditionRun();
        Thread thread = null;
        for (int i = 0; i < 1000; i++) {
            thread = new Thread(additionRun);
            thread.start();
        }
        //休眠2秒,以便1000個線程已經全部執行完成。
        Thread.sleep(2000);
        System.out.print("Count :" + additionRun.count);
    }
}

很簡單的案例,啓動了1000個線程都對count進行+1操作,最後1000個線程運行完之後,打印出結果。我運行了10次,每一次的結果都不一樣。

併發三個重要概念

要解決併發的問題,需要先了解一下三個概念

  1. 原子性 一個或多個操作要麼全部執行完並且執行過程中不會被打斷,要麼都不執行。
    舉例說明:
    1.int i = 2; int a = i; int b = i+1;
    上面三個例子中是否都保證原子性呢?第一個保證原子性,直接將2賦值給常量i,整個過程不能再分,直接一步完成整個操作。第二個不保證原子性,因爲它其實是分爲了幾個步驟,首先給a開闢內存,然後取出i的值,然後將i的值賦給a,這個過程是可以被中斷的,不能保證整個過程能全部執行完畢,所以不能保證原子性。第三個不保證原子性,首先給b開闢內存,然後取出i的值,然後做+1操作得到返回值賦給b,這個過程同樣可能被中斷,不能保證整個過程能全部執行完。
  2. 可見性 可見性是說線程之間都需要訪問同一個共享數據,當這個共享數據發生改變的時候,所有的線程都能自動知道,這兒不在給予舉例說明了,上面的案例中兩個線程處理count的時候,就是不可見的。
  3. 有序性 有序性是指代碼的執行能根據我們書寫代碼的順序進行執行。在編譯器編譯代碼的時候,不一定會會有一個代碼優化過程,我們稱爲“指令重排序”,編譯器不保證代碼的執行順序是按照書寫代碼的順序執行,但是會保證執行結果跟書寫代碼順序結果一致。於多線程中來說,我們不能保證線程的執行順序。
    舉例:int a =1;a = 2; a=3;這三句代碼,編譯器在編譯的時候,並不會每一條語句都進行編譯,只會編譯a = 3,因爲前兩條之間並沒有跟a的值產生依賴關係,所以是無效代碼,所以前兩句代碼編譯器不會進行編譯。
開篇提出的第三個問題,這兒就可以給出解答了,控制併發問題,其實就是要保證原子性,可見性,有序性。,當然了,別沒有給出實例,和實際API方案進行解決,具體的解決方法,會在本系列後續文章中講解!


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