本編文章都是基于下圖這個,計算機cpu 、緩存、內存、線程之間的關系;
一、緩存一致性問題
計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程中,勢必涉及到數據的讀取和寫入。由于程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由于CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,因此如果任何時候對數據的操作都要通過和內存的交互來進行,會大大降低指令執行的速度。因此在CPU里面就有了高速緩存。
當程序在運行過程中,會將運算需要的數據從主存復制一份到CPU的高速緩存當中,那么CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之后,再將高速緩存中的數據刷新到主存當中。
中間的高速緩存就是cpu和內存的中間過程。
但是在多線程,每個線程在不同的cpu中運行時,每個線程分別讀取內存中的值存入各自所在的CPU的高速緩存當中,cpu對數據改變后,就造成了緩存一致性的問題,通常稱這種被多個線程訪問的變量為共享變量。
也就是說,如果一個變量在多個CPU中都存在緩存(一般在多線程編程時才會出現),那么就可能存在緩存不一致的問題。
為了解決緩存不一致性問題,通常來說有以下2種解決方法:
1)通過synchronized鎖的方式
2)通過緩存一致性協議
二、并發編程中的三個概念
原子性:即一個操作或者多個操作 要么全部執行并且執行的過程不會被任何因素打斷,要么就都不執行,相當于事物的概念。
可見性:可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
有序性:即程序執行的順序按照代碼的先后順序執行,因為有指令重排序問題;
指令重排序,一般來說,處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先后順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。這種情況在單線程下沒有問題,但是在多線程下有可能出現問題。
也就是說,要想并發程序正確地執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程序運行不正確。
三、synchronized
同步鎖可以保證并發編程中的原子性、可見性、有序性;但是效率比較低。
四、volatile
-
保證可見性問題:
一個共享變量被volatile修飾時,當CPU對該變量有寫操作時,它會保證修改的值會立即被更新到主內存中,并會發出信號通知其他CPU將該變量的緩存設置為無效狀態,因此當其他CPU需要讀取這個變量時,發現自己的高速緩存中該變量是無效的,那么它就會從內存重新讀取。而普通的共享變量不能保證可見性,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。
可見性只能保證每次讀取的是最新的值
-
保證有序性問題:
當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對后面的操作可見;在其后面的操作肯定還沒有進行;在進行指令優化時,不能將在對volatile變量訪問的語句放在其后面執行,也不能把volatile變量后面的語句放到其前面執行
不能保證原子性問題:
測試代碼:
package com.test.jvm;
public class Test {
public volatile int i = 0;
public void increase(){ //可以添加關鍵字synchronized看結果不同
i++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int x =0; x<10; x++){
new Thread(){@Override
public void run() {
for(int y=0; y<1000; y++){
test.increase();
}
}
}.start();
}
while(Thread.activeCount()>1){ //保證前面的線程都執行完
Thread.yield();
System.out.println(test.i);
}
}
}
最后i的結果并不是10000 ,總是小于10000;
解釋:
假如某個時刻變量 i 的值為10,
cpu1中線程A對變量進行自增操作,線程A先讀取了變量 i 的原始值,然后線程A被阻塞了;
然后cpu2中線程B對變量進行自增操作,線程B也去讀取變量 i 的原始值,由于線程A只是對變量 i 進行讀取操作,而沒有對變量進行修改操作,所以不會導致線程B的工作內存中緩存變量 i 的緩存無效,所以線程B中 i 的值10,然后進行加1操作,并把11寫入工作內存,最后寫入主存。
然后線程A接著進行加1操作,由于已經讀取了 i 的值,注意此時在線程A的工作內存中 i
的值仍然為10,所以線程A對 i 進行加1操作后 i 的值為11,然后將11寫入工作內存,最后寫入主存。
那么兩個線程分別進行了一次自增操作后,i 只增加了1。
解釋到這里,可能有朋友會有疑問,不對啊,前面不是保證一個變量在修改volatile變量時,會讓緩存行無效嗎?然后其他線程去讀就會讀到新的值,對,這個沒錯。但是要注意,線程A對變量進行讀取操作之后,被阻塞了的話,并沒有對 i 值進行修改。然后雖然volatile能保證線程B對變量 i 的值讀取是從內存中讀取的,但是線程A沒有進行修改,所以線程B根本就不會看到修改的值。
總結:
使用volatitle關鍵字要保證的兩個條件:
1) 對變量的寫操作不依賴于當前值
2) 該變量沒有包含在其他變量中