大家好,我是"java繼父"伯約,假如這篇對大家有幫助的話求一個贊,另外文章末尾放了我從小白到架構師多年的學習資料。
1.為什么需要多線程
眾所周知,CPU、內存、I/O 設備的速度是有極大差異的,為了合理利用 CPU 的高性能,平衡這三者的速度差異,計算機體系結構、操作系統、編譯程序都做出了貢獻,主要體現為:
- CPU 增加了緩存,以均衡與內存的速度差異;// 導致
可見性
問題 - 操作系統增加了進程、線程,以分時復用 CPU,進而均衡 CPU 與 I/O 設備的速度差異;// 導致
原子性
問題 - 編譯程序優化指令執行次序,使得緩存能夠得到更加合理地利用。// 導致
有序性
問題
2.線程不安全示例
如果多個線程對同一個共享數據進行訪問而不采取同步操作的話,那么操作的結果是不一致的。
以下代碼演示了 1000 個線程同時對 cnt 執行自增操作,操作結束之后它的值有可能小于 1000。
public class ThreadUnsafeExample {private int cnt = 0;public void add() {cnt++;}public int get() {return cnt;}
}
public static void main(String[] args) throws InterruptedException {final int threadSize = 1000;ThreadUnsafeExample example = new ThreadUnsafeExample();final CountDownLatch countDownLatch = new CountDownLatch(threadSize);ExecutorService executorService = Executors.newCachedThreadPool();for (int i = 0; i < threadSize; i++) {executorService.execute(() -> {example.add();countDownLatch.countDown();});}countDownLatch.await();executorService.shutdown();System.out.println(example.get());
}
997 // 結果總是小于1000
3.并發出現問題的根源: 并發三要素
上述代碼輸出為什么不是1000? 并發出現問題的根源是什么?
可見性: CPU緩存引起
可見性:一個線程對共享變量的修改,另外一個線程能夠立刻看到。
舉個簡單的例子,看下面這段代碼:
//線程1執行的代碼
int i = 0;
i = 10;//線程2執行的代碼
j = i;
假若執行線程1的是CPU1,執行線程2的是CPU2。由上面的分析可知,當線程1執行 i =10這句時,會先把i的初始值加載到CPU1的高速緩存中,然后賦值為10,那么在CPU1的高速緩存當中i的值變為10了,卻沒有立即寫入到主存當中。
此時線程2執行 j = i,它會先去主存讀取i的值并加載到CPU2的緩存當中,注意此時內存當中i的值還是0,那么就會使得j的值為0,而不是10.
這就是可見性問題,線程1對變量i修改了之后,線程2沒有立即看到線程1修改的值。
原子性: 分時復用引起
原子性:即一個操作或者多個操作 要么全部執行并且執行的過程不會被任何因素打斷,要么就都不執行。
舉個簡單的例子,看下面這段代碼:
int i = 1;// 線程1執行
i += 1;// 線程2執行
i += 1;
這里需要注意的是:i += 1
需要三條 CPU 指令
- 將變量 i 從內存讀取到 CPU寄存器;
- 在CPU寄存器中執行 i + 1 操作;
- 將最后的結果i寫入內存(緩存機制導致可能寫入的是 CPU 緩存而不是內存)。
由于CPU分時復用(線程切換)的存在,線程1執行了第一條指令后,就切換到線程2執行,假如線程2執行了這三條指令后,再切換會線程1執行后續兩條指令,將造成最后寫到內存中的i值是2而不是3。
有序性: 重排序引起
有序性:即程序執行的順序按照代碼的先后順序執行。舉個簡單的例子,看下面這段代碼:
int i = 0;
boolean flag = false;
i = 1; //語句1
flag = true; //語句2
上面代碼定義了一個int型變量,定義了一個boolean類型變量,然后分別對兩個變量進行賦值操作。從代碼順序上看,語句1是在語句2前面的,那么JVM在真正執行這段代碼的時候會保證語句1一定會在語句2前面執行嗎? 不一定,為什么呢? 這里可能會發生指令重排序(Instruction Reorder)。
在執行程序時為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分三種類型:
- 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
- 指令級并行的重排序。現代處理器采用了指令級并行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
- 內存系統的重排序。由于處理器使用緩存和讀 / 寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
從 java 源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
????????上述的 1 屬于編譯器重排序,2 和 3 屬于處理器重排序。這些重排序都可能會導致多線程程序出現內存可見性問題。對于編譯器,JMM 的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對于處理器重排序,JMM 的處理器重排序規則會要求 java 編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel 稱之為 memory fence)指令,通過內存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。