一 . 可見性(visibility)
volatile關鍵字修飾的變數,如果值發生了改變,其他執行緒會立刻獲取到,從而避免了出現髒讀的情況。
1 public class TestVolatile { 2 3 public static void main(String[] args) { 4 MyData myData = new MyData(); 5 new Thread(new Runnable() { 6 @Override 7 public void run() { 8 System.out.println("進入運算元據執行緒"); 9 try { 10 Thread.sleep(1000); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 //呼叫方法 賦值 15 myData.changeData(); 16 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 17 } 18 },"運算元據執行緒").start(); 19 20 // 主執行緒檢視資料是否改了 21 while (myData.data == 0){ 22 23 } 24 System.out.println("main執行緒結束"); 25 } 26 } 27 28 class MyData{ 29 int data = 0; 30 public void changeData(){ 31 this.data = 2020; 32 } 33 34 }
如上面程式碼,有兩個執行緒在操作MyDdata資料類,看一下執行結果
從結果可以看出,main執行緒一直就沒有獲取到資料更新資訊,記憶體中的資料儲存用圖直觀的看一下
main執行緒的記憶體執行緒並沒獲取到資料更新。
下面變數加上volatile的效果
1 public class TestVolatile { 2 3 public static void main(String[] args) { 4 MyData myData = new MyData(); 5 new Thread(new Runnable() { 6 @Override 7 public void run() { 8 System.out.println("進入運算元據執行緒"); 9 try { 10 Thread.sleep(1000); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 //呼叫方法 賦值 15 myData.changeData(); 16 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 17 } 18 },"運算元據執行緒").start(); 19 20 // 主執行緒檢視資料是否改了 21 while (myData.data == 0){ 22 23 } 24 System.out.println("main執行緒結束"); 25 } 26 } 27 28 class MyData{ 29 volatile int data = 0; 30 public void changeData(){ 31 this.data = 2020; 32 } 33 34 }
看一下執行結果
發現main方法已經獲取到了資料更新。從而驗證了volatile的可見性。
二 . 無法保證原子性
直接上程式碼
1 public class TestVolatile1 { 2 3 public static void main(String[] args) { 4 MyData1 myData = new MyData1(); 5 6 new Thread(new Runnable() { 7 @Override 8 public void run() { 9 //呼叫方法 賦值 10 myData.changeData(); 11 for(int i = 0;i < 9999;i++) { 12 myData.changeData(); 13 } 14 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 15 } 16 },"執行緒1").start(); 17 18 19 new Thread(new Runnable() { 20 @Override 21 public void run() { 22 //呼叫方法 賦值 23 myData.changeData(); 24 for(int i = 0;i < 9999;i++) { 25 myData.changeData(); 26 } 27 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 28 } 29 },"執行緒2").start(); 30 31 new Thread(new Runnable() { 32 @Override 33 public void run() { 34 //呼叫方法 賦值 35 myData.changeData(); 36 for(int i = 0;i < 9999;i++) { 37 myData.changeData(); 38 } 39 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 40 } 41 },"執行緒3").start(); 42 43 new Thread(new Runnable() { 44 @Override 45 public void run() { 46 //呼叫方法 賦值 47 myData.changeData(); 48 for(int i = 0;i < 9999;i++) { 49 myData.changeData(); 50 } 51 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 52 } 53 },"執行緒4").start(); 54 55 while (Thread.activeCount() > 1) { 56 Thread.yield(); 57 } 58 59 System.out.println("最終數值 :"+myData.data); 60 } 61 } 62 63 class MyData1{ 64 volatile int data = 0; 65 public void changeData(){ 66 data++; 67 } 68 69 }
我們們可以預測一下,如果正常的話,我們們應該得到的最終資料應該是40000 ,但結果如下
可以看到最終資料並不是我們想要的結果,多執行緒同時操作volatile修飾變數,無法保證資料的原子性。
那如何解決這個問題呢,用sychornized,可以處理,但是這是重量級鎖,不推薦使用,還可以用 AtomicInteger 來處理這個情況實現程式碼如下
1 import java.util.concurrent.atomic.AtomicInteger; 2 3 public class TestVolatile1 { 4 5 public static void main(String[] args) { 6 MyData1 myData = new MyData1(); 7 8 new Thread(new Runnable() { 9 @Override 10 public void run() { 11 //呼叫方法 賦值 12 myData.changeData(); 13 for(int i = 0;i < 9999;i++) { 14 myData.changeData(); 15 } 16 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 17 } 18 },"執行緒1").start(); 19 20 21 new Thread(new Runnable() { 22 @Override 23 public void run() { 24 //呼叫方法 賦值 25 myData.changeData(); 26 for(int i = 0;i < 9999;i++) { 27 myData.changeData(); 28 } 29 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 30 } 31 },"執行緒2").start(); 32 33 new Thread(new Runnable() { 34 @Override 35 public void run() { 36 //呼叫方法 賦值 37 myData.changeData(); 38 for(int i = 0;i < 9999;i++) { 39 myData.changeData(); 40 } 41 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 42 } 43 },"執行緒3").start(); 44 45 new Thread(new Runnable() { 46 @Override 47 public void run() { 48 //呼叫方法 賦值 49 myData.changeData(); 50 for(int i = 0;i < 9999;i++) { 51 myData.changeData(); 52 } 53 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 54 } 55 },"執行緒4").start(); 56 57 while (Thread.activeCount() > 1) { 58 Thread.yield(); 59 } 60 61 System.out.println("最終數值 :"+myData.data); 62 } 63 } 64 65 class MyData1{ 66 AtomicInteger data = new AtomicInteger(); 67 public void changeData(){ 68 data.getAndIncrement(); 69 } 70 71 }
執行結果如下
如此資料原子性問題便解決了。
三 . 指令重排
在JVM在編譯Java程式碼的時候,或者CPU在執行JVM位元組碼的時候,對指令順序進行重新排序。在不改變程式執行結果的前提下,優化程式的執行效率(不改變單執行緒下的程式執行結果)
看一段簡單的程式碼
1 public class Data { 2 3 int a = 1; //步驟1 4 int b = 2; //步驟2 5 int c = a+b; //步驟3 6 7 }
單執行緒下,程式碼執行結果c的結果3,但是在執行的過程時候並不一定是1 , 2,3這個執行順序,在發生指令重排後,可能是2,1,3。單執行緒下對工程並沒有什麼影響。
但是如果是多執行緒,就會出現問題。檢視如下方法
1 public class Volatile { 2 3 int a = 1; 4 boolean flag = false; 5 6 public void dosome1() { 7 a = 2;// 步驟1 8 flag = true; //步驟2 9 } 10 11 public void dosome2() { 12 if(flag){ 13 int b = a+a; // 步驟3 14 } 15 } 16 }
上面的程式碼步驟3其實是兩個步驟,為了好理解,可以看成為一個步驟。
如果執行緒A 操作dosome1 而執行緒而B 操作dosome2 如果不發生指令重排
可能順序可能是 1,2,3 b=4 ,這也是我們期望的,
還會出現以下順序
1,3,2 3,1,2 這兩種可能性,如果是這兩個,代表不符合條件,沒有宣告b變數。
但是如果發生重排後,因為1,2沒有依賴關係,很有可能發生指令重排,那名執行的結果就可能出現以下順序
2,3,1 如果出現這個順序,就會宣告變數b,結果為2;這個結果就會很恐怖了,就好比我們做了一個工程,每次執行的結果無法確定。這必然是不行的。為了解決這個問題,我們便可以用volatile來修飾變數。當然sychornized也可以解決。
重排是個比較麻煩的過程,這是一個簡單理解,後續再做詳細的探討。