多執行緒之美1一volatile
阿新 • • 發佈:2019-11-17
目錄
一、java記憶體模型
1.1、抽象結構圖
1.2、概念介紹
二、volatile詳解
2.1、概念
2.2、保證記憶體可見性
2.3、不保證原子性
2.4、有序性
一、java記憶體模型
1.1、抽象結構圖
1.2、概念介紹
java 記憶體模型
即Java memory model(簡稱JMM), java執行緒之間的通訊由JMM控制,決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。
多執行緒通訊通常分為2類:共享記憶體和訊息傳遞
JMM採用的就是共享記憶體來實現執行緒間的通訊,且通訊是隱式的,對程式開發人員是透明的,所以在瞭解其原理了,才會對執行緒之間通訊,同步,記憶體可見性問題有進一步認識,避免開發中出錯。
執行緒之間如何通訊?
在java中多個執行緒之間要想通訊,如上圖所示,每個執行緒在需要操作某個共享變數時,會將該主記憶體中這個共享變數拷貝一份副本存在在自己的本地記憶體(也叫工作記憶體,這裡只是JMM的一個抽象概念,即將其籠統看做一片記憶體區域,用於每個執行緒存放變數,實際涉及到快取,暫存器和其他硬體),執行緒操作這個副本,比如 int i = 1;一個執行緒想要進行 i++操作,會先將變數 i =1 的值先拷貝到自己本地記憶體操作,完成 i++,結果 i=2,此時主記憶體中的值還是1,線上程將結果重新整理到主記憶體後,主記憶體值就更新為2,資料達到一致了。 如果執行緒A,執行緒B同時將 主記憶體中 i =1拷貝副本到自己本地記憶體,執行緒A想要 將i+1,而執行緒B想要將 int j=i,將賦值給j,那麼如何保證執行緒之間的協作,此時就會涉及到執行緒之間的同步以及記憶體可見性問題了。(後文分析synchronized/lock) 那執行緒之間實現通訊需要經過2個步驟,藉助主記憶體為中間媒介: 執行緒A (傳送訊息)-->(接收訊息) 執行緒B 1、執行緒A將本地記憶體共享變數值重新整理到主記憶體中,更新值; 2、執行緒B從主記憶體中讀取已更新過的共享變數;
共享記憶體中涉及到哪些變數稱為共享變數?
這裡的共享記憶體指的是jvm中堆記憶體中,所有堆記憶體線上程之間共享,因為棧中儲存的是方法及其內部的區域性變數,不在此涉及。 共享變數:對於多執行緒之間能夠共同操作的變數,包含例項域,靜態域,陣列元素。即有成員變數,靜態變數等等, 不涉及到區域性變數(所以區域性變數不涉及到記憶體可見性問題)
多執行緒在java記憶體模型中涉及到三個問題
- 可見性
- 原子性
- 有序性(涉及指令重排序)
二、volatile詳解
2.1、概念
-1、volatile 是 java中的關鍵字,可修飾字段,可以保證共享變數的在記憶體的可見性,有序性,不保證原子性。 -2、作用:在瞭解java記憶體模型後,才能更加了解volatile在JMM中的作用,volatile在JMM中為了保證記憶體的可見性,即是執行緒之間操作共享變數的可見性。
- volatile寫和讀的記憶體語義
volatile 寫的記憶體語義:
當寫一個volatile修飾的共享變數時,JMM會把該執行緒的本地記憶體的共享變數副本值重新整理到主記憶體中;
volatile 讀的記憶體語義:
當讀一個volatile修飾的共享變數時,JMM會將該執行緒的本地記憶體的共享變數副本置為無效,要求執行緒重新去主記憶體中獲取最新的值。
- java記憶體模型控制與volatile衝突嗎?什麼區別?
不衝突!java記憶體模型控制執行緒工作記憶體與主記憶體之間共享變數會同步,即執行緒從主記憶體中讀一份副本到工作記憶體,又重新整理到主記憶體,那怎麼還需要 volatile來保證可見性,不是JMM自己能控制嗎,一般情況下JMM可以控制 2份記憶體資料一致性,但是在多執行緒併發環境下,雖然最終執行緒工作記憶體中的共享變數會同步到主記憶體,但這需要時間和觸發條件,執行緒之間同時操作共享變數協作時,就需要保證每次都能獲取到主記憶體的最新資料,保證看到的工作變數是最後一次修改後的值,這個JMM沒法控制保證,這就需要volatile或者後文要講的 synchronized和鎖的同步機制來實現了。
2.2、保證記憶體可見性
1、多個執行緒出現記憶體不可見問題示例
/** * @author zdd * Description: 測試執行緒之間,記憶體不可見問題 */ public class TestVisibilityMain { private static boolean isRunning = true; // 可嘗試新增volatile執行,其餘不變,檢視執行緒A是否被停止 //private static volatile boolean isRunning = true; public static void main(String[] args) throws InterruptedException { //1,開啟執行緒A,讀取共享變數值 isRunning,預設為true new Thread(()->{ // --> 此處用的lamda表示式,{}內相當於Thread的run方法內部需執行任務 System.out.println(Thread.currentThread().getName() + "進入run方法"); while (isRunning == true) { } System.out.println(Thread.currentThread().getName()+"被停止!"); },"A").start(); //2,主執行緒休眠1s, 確保執行緒A先被排程執行 TimeUnit.SECONDS.sleep(1); //3,主執行緒修改共享變數值 為flase,驗證執行緒A是否能夠獲取到最新值,跳出while迴圈 --> 驗證可見性 isRunning =false; System.out.println(Thread.currentThread().getName() +"修改isRunning為: " + isRunning); } }
執行結果如下圖:
- 2、一個容易忽視的問題
上面程式碼 while裡面是一個空迴圈,沒有操作,如果我在裡面加一句列印語句,執行緒A會被停止,這是怎麼回事呢?
原:while (isRunning == true) {}
改1:
while (isRunning == true) {
System.out.println("進入迴圈");
}
原來 println方法裡面加了 synchronized關鍵字,在加了鎖既保證原子性,也保證了可見性,會實現執行緒的工作記憶體與主記憶體共享變數的同步。
原始碼如下:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
改2:
while (isRunning == true) {
//改為這樣,也可以停止執行緒A
synchronized (TestVisibilityMain.class){}
}
2.3、不保證原子性
- 1、示例程式碼
/**
* @author zdd
* Description: 測試volatile的不具有原子性
*/
public class TestVolatileAtomic {
private static volatile int number;
//開啟執行緒數
private static final int THREAD_COUNT =10;
//執行 +1 操作
public static void increment() {
//讓每個執行緒進行加1次數大一些,能夠更容易出現volatile對複合操作(i++)沒有原子性的錯誤
for (int i = 0; i < 10000; i++) {
number++;
}
System.out.println(Thread.currentThread().getName() +"的number值: "+number);
}
public static int getNumber() {
return number;
}
public static void main(String[] args) throws InterruptedException {
TestVolatileAtomic volatileAtomic = new TestVolatileAtomic();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i]=
new Thread(()->{
// 做迴圈自增操作
volatileAtomic.increment();
System.out.println(Thread.currentThread().getName() +"的number值: "+volatileAtomic.getNumber());
},"thread-"+i);
}
for (int i = 0; i <10; i++) {
//開啟執行緒
threads[i].start();
}
//主執行緒休眠4s,確保上面執行緒都執行完畢
TimeUnit.SECONDS.sleep(4);
System.out.println("執行完畢,number最終值為:"+volatileAtomic.getNumber());
}
}
執行結果:number的最後值不一定是 10*10000= 100000的結果
- 2、解決上訴問題
//1,increment()方法上加上 synchronized關鍵字同步
public static synchronized void increment() {
//讓每個執行緒進行加1次數大一些,能夠更容易出現volatile對複合操作(i++)沒有原子性的錯誤
for (int i = 0; i < 10000; i++) {
number++;
}
System.out.println(Thread.currentThread().getName() +"的number值: "+number);
}
//2,使用Lock,使用其實現類可重入鎖 ReentrantLock
static Lock lock = new ReentrantLock();
//執行 +1 操作
public static void increment() {
lock.lock();
try {
for (int i = 0; i < 10000; i++) {
number++;
}
System.out.println(Thread.currentThread().getName() + "的number值: " + number);
} finally {
lock.unlock();
}
}
執行結果如圖:
- 3、原因分析
對單個volatile變數的讀/寫具有原子性,而對像 i++這種複合操作不具有原子性。
上面程式碼 i++操作可以分為3個步驟
-1 先讀取變數i的值 i
-2 進行i+1操作 temp= i+1
-3 修改i的值 i= temp
比如:比如線上程A,B同時去操作共享變數i, i的初始值為10,A,B同時去獲取i的值,A對i進行 temp =i+1,此時i的值還沒變, 執行緒B也對i進行 temp=i+1了,執行緒A執行i=temp的操作,i的值變為11,此時由於 volatile可見性,會重新整理A的 i值到主記憶體,主記憶體中i此時也更新為11了,執行緒B接收到通知自己i無效了,重新讀取i=11,雖然i=11,但是已經進行過 temp= i+1了,此時temp =11,執行緒B繼續第三步,i=temp =11, 預期結果是i被A,B自增各一次,結果i=12,現在為11,出現數據錯誤。
2.4、有序性
- 重排序
-1,重排序概念:重排序是編譯器和處理器為了優化程式效能而對指令序列重新排序的一種手段
即:程式設計師編寫的程式程式碼的順序,在實際執行的時候是不一樣的,這其中編譯器和處理器在不影響最終執行結果的基礎上會做一些優化調整,有重新排序的操作,為了提高程式執行的併發效能。
-2,重排序分類: 編譯重排序,處理器重排序
-4,單執行緒下,重排序沒有問題,但是在多執行緒環境下,可能會破壞程式的語義.
- volatile 防止重排序保證有序性
為了實現volatile的記憶體語義,JMM會限制編譯器和處理器重排序
-1 制定了重排序規則表編譯器防止編譯器重排序
volatile重排序規則表(圖摘自書-併發程式設計的藝術)
-2 插入記憶體屏障防止處理器重排序
參考資料:
1、Java併發程式設計的藝術- 方騰飛
2、java多執行緒程式設計核心技術- 高洪