可見性
什麼是可見性?
一個線程對主內存的修改可以及時的被其他線程觀察到
導致共享變量在線程間不可見的原因
- 線程交叉執行
- 重排序結合線程交叉執行
- 共享變量更新後的值沒有在工作內存與主存間及時更新
JVM處理可見性
JVM對於可見性,提供了synchronized和volatile
JMM關於synchronized的兩條規定:
- 線程解鎖前,必須把共享變量的最新值刷新到主內存
- 線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值(注意:加鎖與解鎖是同一把鎖)
Volatile:通過加入內存屏障和禁止重排序優化來實現
- 對volatile變量寫操作時,會在寫操作後加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存。
- 對volatile變量讀操作時,會在讀操作前加入一條load屏障指令,從主內存中讀取共享變量。
volatile的屏障操作都是cpu級別的。
適合狀態驗證,不適合累加值,volatile關鍵字不具有原子性
舉個例子:我們仍用高併發學習(二)中的例子來說明,對一個int型數值的多線程讀寫操作。我們將count變量用volatile來修飾:
public class CountExample {
//請求總數
public static int clientTotal = 5000;
//同時併發執行的線程數
public static int threadTotal = 200;
//計數 *
public static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
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 (InterruptedException e) {
log.error("excption",e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();//保證信號量減爲0
executorService.shutdown();//關閉線程池
log.info("count:{}",count);
}
private static void add(){
count++;
}
}
多次運行代碼我們發現:count的最終結果並不是預期的5000,而是有時爲5000,但是大多數時間比5000小,這是爲什麼呢?原因在於對count++的操作中,jvm對count做了三步操作:
1、從主存中取出count的值放入工作變量 count
2、對工作變量中的count進行+1
3、將工作變量中的count刷新回主存中
在單線程執行此操作絕對沒有問題,但是在多線程環境中,假設有兩個線程A、B同時執行count++操作,某一刻A與B同時讀取主存中count的值,然後在自己線程對應的工作空間中對count+1,最後又同時將count+1的值寫回主存。到此,count+1的值被寫回主存兩遍,所以導致最終的count值小了1。在整體程序執行過程中,該事件發生一次或多次,自然結果就不正確。
那麼volatile適合做什麼呢?其實它比較適合做狀態標記量(不會涉及到多線程同時讀寫的操作),而且要保證兩點:
(1)對變量的寫操作不依賴於當前值
(2)該變量沒有包含在具有其他變量的不變的式子中
例如:
volatile boolean inited = false;
//線程一:
context = loadContext();
inited = true;
//線程二:
while(!inited){
sleep();
}
doSomethingWithConfig(context);
有序性
什麼是有序性?
Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。
關於重排序,詳情見:高併發學習(二)– 4、亂序執行優化
java中保證有序性
java提供了 volatile、synchronized、Lock可以用來保證有序性
另外,java內存模型具備一些先天的有序性,即不需要任何手段就能得到保證的有序性。通常被我們成爲happens-before原則(先行發生原則)。如果兩個線程的執行順序無法從happens-before原則推導出來,那麼就不能保證它們的有序性,虛擬機就可以對它們進行重排序。
【以下規則來自於《深入理解java虛擬機》】
- 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作
- 鎖定規則:一個unlock操作先行發生於後面對同一個鎖的lock操作
- volatile變量規則:對一個變量的寫操作先行發生於後面對這個變量的讀操作(重要)
- 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
- ——————————————————————————————————————————————
- 線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個動作
- 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
- 線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
- 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始