在並發編程中,需要處理的兩個關鍵問題:線程之間如何通信以及線程之間如何同步。
通信是指線程之間以或者機制交換信息,java的並發采用的是共享內存模型,線程之間共享程序的公共狀態,通過讀寫內存中的公共狀態進行隱式通信。
同步是是指程序中用於控制不同線程間操作發生相對順序的機制。
最開始首先應該知道計算機中的緩存在其中起的作用
CPU Cache(高速緩存):由於計算機的存儲設備與處理器的處理設備有着幾個數量級的差距,所以現代計 算機都會加入一層讀寫速度與處理器處理速度接近相同的高級緩存來作為內存與處理器之間的緩沖,將運 算使用到的數據復制到緩存中,讓運算能夠快速的執行,當運算結束后,再從緩存同步到內存之中,這 樣,CPU就不需要等待緩慢的內存讀寫了。
主(內)存:一個計算機包含一個主存,所有的CPU都可以訪問主 存,主存比緩存容量大的多(CPU訪問緩存層的速度快於訪問主存的速度!但通常比訪問內存寄存器的速度還是要慢點)
運作原理:通常情況下,當一個CPU要讀取主存(RAM - Main Mernory)的時候,他會將主存中的數據讀 取到CPU緩存中,甚至將緩存內容讀到內部寄存器里面,然后再寄存器執行操作,當運行結束后,會 將寄存器中的值刷新回緩存中,並在某個時間點將值刷新回主存。
為什么需要CPU Cache?
答:CPU 的頻率太快了,快到主存跟不上,這樣在處理器時鍾周期內,CPU常常需要等待主存,浪費資源。 所以cache 的出現,是為了緩解 CPU 和內存之間速度的不匹配問題 結構:cpu-> cache-> memory).
什么是java的內存模型?


每個線程之間共享變量都存放在主內存里面,每個線程都有一個私有的本地內存,本地內存是Java內存模型中抽象的概念,並不是真實存在的(他涵蓋了緩存寫緩沖區。寄存器,以及其他硬件的優化) 本地內存中存儲了以讀或者寫共享變量的拷貝的一個副本。
注意:由於工作內存(緩沖區)僅對自己的處理器可見,它會導致處理器質質性內存操作的順序可能會與內存實際的操作順序不一致,內存的操作順序被重排序了,這是與后面講的指令重排序不同的另一種重排序。
線程一對共享變量的改變想要被線程二看見,就必須執行下面兩個步驟:

1.編譯器優化的重排序(編譯器優化)
2.指令級並行重排序(處理器優化)
3.內存系統的重排序(處理器優化)
是不是所有的語句的執行順序都可以重排呢?
什么是數據依賴性?
如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴。數據依賴分下列三種類型:
名稱 | 代碼示例 | 說明 |
寫后讀 | a = 1;b = a; | 寫一個變量之后,再讀這個位置。 |
寫后寫 | a = 1;a = 2; | 寫一個變量之后,再寫這個變量。 |
讀后寫 | a = b;b = 1; | 讀一個變量之后,再寫這個變量。 |
上面三種情況,只要重排序兩個操作的執行順序,程序的執行結果將會被改變。所以,編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序。也就是說:在單線程環境下,指令執行的最終效果應當與其在順序執行下的效果一致,否則這種優化便會失去意義。這句話有個專業術語叫做as-if-serial semantics (as-if-serial語義)
int num1=1;//第一行
int num2=2;//第二行
int sum=num1+num;//第三行
- 單線程:第一行和第二行可以重排序,但第三行不行
- 重排序不會給單線程帶來內存可見性問題
- 多線程中程序交錯執行時,重排序可能會照成內存可見性問題。
可見性分析:
導致共享變量在線程間不可見的原因:
- 線程的交叉執行
- 重排序結合線程交叉執行
- 共享變量更新后的值沒有在工作內存與主內存間及時更新
答案是:不一定能看到。
由於操作1和操作2沒有數據依賴關系,編譯器和處理器可以對這兩個操作重排序;同樣,操作3和操作4沒有數據依賴關系,編譯器和處理器也可以對這兩個操作重排序。讓我們先來看看,當操作1和操作2重排序時,可能會產生什么效果?
執行順序是:2 -> 3 -> 4 -> 1 (這是完全存在並且合理的一種順序,如果你不能理解,請先了解CPU是如何對多個線程進行時間分配的)操作3和操作4重排序后,因為操作3和操作4存在控制依賴關系。當代碼中存在控制依賴性時,會影響指令序列執行的並行度。為此,編譯器和處理器會采用猜測(Speculation)執行來克服控制相關性對並行度的影響。以處理器的猜測執行為例,執行線程B的處理器可以提前讀取並計算a*a,然后把計算結果臨時保存到一個名為重排序緩沖(reorder buffer ROB)的硬件緩存中。當接下來操作3的條件判斷為真時,就把該計算結果寫入變量i中。
我們可以看出,猜測執行實質上對操作3和4做了重排序。重排序在這里破壞了多線程程序的語義!


package com.xidian.count; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import com.xidian.annotations.ThreadSafe; import lombok.extern.slf4j.Slf4j; @Slf4j @ThreadSafe public class CountExample3 { // 請求總數 public static int clientTotal = 5000; // 同時並發執行的線程數 public static int threadTotal = 200; public static int count = 0; public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}", count); } private synchronized static void add() { count++; } }
volatile實現可見性


volatile關鍵字:
- 能夠保證volatile變量的可見性
- 只能保證單個volatile變量的原子性,對於volatile++這種復合操作不具有原子性
深入來說:通過加入內存屏障和禁止重排序優化來實現的。
-
對volatile變量執行寫操作時,會在寫操作后加入一條store屏障指令
- store指令會在寫操作后把最新的值強制刷新到主內存中。同時還會禁止cpu對代碼進行重排序優化。這樣就保證了值在主內存中是最新的。
-
對volatile變量執行讀操作時,會在讀操作前加入一條load屏障指令
- load指令會在讀操作前把內存緩存中的值清空后,再從主內存中讀取最新的值。


package com.xidian.count; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import com.xidian.annotations.NotThreadSafe; import lombok.extern.slf4j.Slf4j; @Slf4j @NotThreadSafe public class CountExample4 { // 請求總數 public static int clientTotal = 5000; // 同時並發執行的線程數 public static int threadTotal = 200; public static volatile int count = 0; public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}", count); } private static void add() { count++; // 1、count 從主存中取出count的值 // 2、+1 在工作內存中執行+1操作 // 3、count 將count的值寫回主存 //及時將count用vilatile修飾,每次從主存中取到的都是最新的值,可是當多個線程同時取到最新的值,執行+1操作,當刷新到主存中的時候會覆蓋結果,從而丟失一些+1操作 } }
volatile實現共享變量內存可見性有一個條件,就是對共享變量的操作必須具有原子性。比如 num = 10; 這個操作具有原子性,但是 num++ 或者num--由3步組成,並不具有原子性,所以是不行的。
假如num=5,此時有線程A從主內存中獲取num的值,並執行++,但在還未見修改寫入主內存中,又有線程B取得num的值,對其進行++操作,造成丟失修改,明明執行了2次++,num的值卻只增加了1.
-
對變量的寫入操作不依賴其當前值
- 不滿足:number++、count=count*5
- 滿足:boolean變量、記錄溫度變化的變量等
-
該變量沒有包含在具有其他變量的不變式中
- 不滿足:不變式 low<up
綜上,volatile特別適合用來做線程標記量,如下圖
synchronized和volatile的比較;
- synchronized鎖住的是變量和變量的操作,而volatile鎖住的只是變量,而且該變量的值不能依賴它本身的值,volatile算是一種輕量級的同步鎖
- volatile不需要加鎖,比synchronized更加輕量級,不會阻塞線程。
- 從內存可見性角度講,volatile讀相當於加鎖,volatilexie相當於解鎖。
- synchronized既能保證可見性,又能保證原子性,而volatile只能保證可見性,無法保證原子性。
注:由於voaltile比synchronized更加輕量級,所以執行的效率肯定是比synchroized更高。在可以保證原子性操作時,可以盡量的選擇使用volatile。在其他不能保證其操作的原子性時,再去考慮使用synchronized。
有序性
Happens-before原則,先天有序性,即不需要任何額外的代碼控制即可保證有序性,java內存模型一個列出了八種Happens-before規則,如果兩個操作的次序不能從這八種規則中推倒出來,則不能保證有序性。
第一條規則要注意理解,這里只是程序的運行結果看起來像是順序執行,雖然結果是一樣的,jvm會對沒有變量值依賴的操作進行重排序,這個規則只能保證單線程下執行的有序性,不能保證多線程下的有序性。
總結