JAVA多執行緒安全的三大特性 + synchronized和volatile
文章目錄
執行緒安全的三大特性
在多執行緒程式設計中,我們通常會遇到以下三個問題:原子性問題,可見性問題和有序性問題。首先我們看一下這三個特性概念:
原子性
原子性是指一個操作或者多個操作要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
先來看一個例子:使用程式實現一個計數器,期望得到的結果是10000,程式碼如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class UnsafeCount {
public static volatile int count = 0; //後面會解釋volatile關鍵字
public static void inc() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(Integer.MAX_VALUE);
for (int i = 0; i < 10000; i++) {
service.execute(new Runnable() {
@Override
public void run() {
UnsafeCount.inc();
}
});
}
service.shutdown();
//避免出現main主執行緒先跑完而子執行緒還沒結束,在這裡給予一個關閉時間
service.awaitTermination(3000,TimeUnit.SECONDS);
System.out.println("執行結果:UnsafeCount.count=" + UnsafeCount.count);
}
}
控制檯輸出:
執行結果:UnsafeCount.count=9996
最終結果是9996(count的值不是固定的),並非是我們期望的10000,這正是因為執行緒不安全導致的錯誤結果。
原因分析:
執行緒1 | 執行緒2 |
---|---|
讀取 count 的值, 假設此時count = 0 | 等待 |
將"0"加1變為1 | 等待 |
時間片用完,執行執行緒二 | 讀取 count的值 , count為0 (此時執行緒一還沒有修改 count ) |
等待 | 將"0"加1變為1 |
等待 | 修改 count 的值 ,此時 count = 1; |
修改 count 的值 ,此時 count = 1 | 時間片用完,執行執行緒一 |
通過上面的分析可見雖然執行了兩次count++但最終count的值為1,這就是由於count++為非原子操作引起的。
可見性
可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。
產生這樣問題的原因是由於java執行緒通訊是通過共享記憶體的方式進行通訊的,而為了加快執行的速度,執行緒一般是不會直接操作記憶體的,而是操作快取。
java執行緒記憶體模型:
- Java所有變數都儲存在主記憶體中
- 每個執行緒都有自己獨立的工作記憶體,裡面儲存該執行緒的使用到的變數副本(該副本就是主記憶體中該變數的一份拷貝)
- 執行緒對共享變數的所有操作都必須在自己的工作記憶體中進行,不能直接在主記憶體中讀寫
- 不同執行緒之間無法直接訪問其他執行緒工作記憶體中的變數,執行緒間變數值的傳遞需要通過主記憶體來完成。
執行緒1對共享變數的修改,要想被執行緒2及時看到,必須經過如下2個過程:
- 把工作記憶體1中更新過的共享變數重新整理到主記憶體中
- 將主記憶體中最新的共享變數的值更新到工作記憶體2中
實際上,執行緒操作的是自己的工作記憶體,而不會直接操作主記憶體。如果執行緒對變數的操作沒有刷寫會主記憶體的話,僅僅改變了自己的工作記憶體的變數的副本,那麼對於其他執行緒來說是不可見的。
下面以一個簡單的例子說明:
//執行緒1執行的程式碼
int i = 0;
i = 10;
//執行緒2執行的程式碼
j = i;
執行緒1 | 執行緒2 |
---|---|
讀取 i 的值, 假設此時 i = 0 | 等待 |
修改 i 的值 ,此時 i = 10(假設此時i的值還沒有同步到主記憶體中) | 等待 |
時間片用完,執行執行緒二 | 讀取 i 的值為0( 因為i的值還沒有同步到主記憶體中) |
等待 | 修改 j 的值 ,此時 j = 0; |
假若執行執行緒1的是CPU1,執行執行緒2的是CPU2。由上面的分析可知,當執行緒1執行 i =10這句時,會先把i的初始值載入到CPU1的快取記憶體中,然後賦值為10,那麼在CPU1的快取記憶體當中i的值變為10了,卻沒有立即寫入到主存當中。
此時執行緒2執行 j = i,它會先去主存讀取i的值並載入到CPU2的快取當中,注意此時記憶體當中i的值還是0,那麼就會使得j的值為0,而不是1。這就是可見性問題,執行緒1對變數i修改了之後,執行緒2沒有立即看到執行緒1修改的值。
有序性
有序性是指程式在執行的時候,程式的程式碼執行順序和語句的順序是一致的。
產生這樣問題的原因是由於重排序的緣故。在Java記憶體模型中,為了加快程式的執行速度允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單執行緒程式的執行,卻會影響到多執行緒併發執行的正確性。
舉個例子:
//執行緒A:
context = loadContext();
inited = true;
//執行緒B:
while(!inited ){
sleep
}
doSomethingwithconfig(context);
如果執行緒A發生了重排序執行緒A會變成如下情況:
inited = true;
context = loadContext();
那麼執行緒B就會拿到一個未初始化的context去使用,從而引起錯誤。
因為這個重排序對於執行緒A來說是不會影響執行緒A的正確性的,而如果loadContext()方法被阻塞了,為了增加Cpu的利用率,這個重排序是可能的。
保證執行緒安全的兩個關鍵字
Synchronized
Synchronized能夠實現原子性和可見性;在Java記憶體模型中,synchronized規定,執行緒在互斥程式碼時,先獲得互斥鎖→清空工作記憶體→在主記憶體中拷貝最新變數的副本到工作記憶體→執行完程式碼→將更改後的共享變數的值重新整理到主記憶體中→釋放互斥鎖。
Volatile
Volatile能夠實現可見性和有序性;Volatile實現記憶體可見性是通過store和load指令完成的;也就是對volatile變數執行寫操作時,會在寫操作後加入一條store指令,即強迫執行緒將最新的值重新整理到主記憶體中;而在讀操作時,會加入一條load指令,即強迫從主記憶體中讀入變數的值。但volatile不保證volatile變數的原子性(可以看第一個例子)