提出問題
說到併發,我們首先應該給自己提出下面這三個問題:
- 產生併發的根本原因是什麼?
- 會造成什麼後果?
- 怎麼去控制,處理併發達到我們預期的結果。
在線程的角度來說,內存分爲共享內存和私有內存兩個部分。線程訪問一個共享區的資源時,會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.int i = 2; int a = i; int b = i+1;
上面三個例子中是否都保證原子性呢?第一個保證原子性,直接將2賦值給常量i,整個過程不能再分,直接一步完成整個操作。第二個不保證原子性,因爲它其實是分爲了幾個步驟,首先給a開闢內存,然後取出i的值,然後將i的值賦給a,這個過程是可以被中斷的,不能保證整個過程能全部執行完畢,所以不能保證原子性。第三個不保證原子性,首先給b開闢內存,然後取出i的值,然後做+1操作得到返回值賦給b,這個過程同樣可能被中斷,不能保證整個過程能全部執行完。 - 可見性 可見性是說線程之間都需要訪問同一個共享數據,當這個共享數據發生改變的時候,所有的線程都能自動知道,這兒不在給予舉例說明了,上面的案例中兩個線程處理count的時候,就是不可見的。
- 有序性 有序性是指代碼的執行能根據我們書寫代碼的順序進行執行。在編譯器編譯代碼的時候,不一定會會有一個代碼優化過程,我們稱爲“指令重排序”,編譯器不保證代碼的執行順序是按照書寫代碼的順序執行,但是會保證執行結果跟書寫代碼順序結果一致。於多線程中來說,我們不能保證線程的執行順序。
舉例:int a =1;a = 2; a=3;這三句代碼,編譯器在編譯的時候,並不會每一條語句都進行編譯,只會編譯a = 3,因爲前兩條之間並沒有跟a的值產生依賴關係,所以是無效代碼,所以前兩句代碼編譯器不會進行編譯。