1.volatile是Java虛擬機提供的輕量級的同步機制
-
保證可見性
-
不保證原子性
-
禁止指令重排
2. Java內存模型(JMM)
JMM(Java內存模型Java Memory Model,簡稱JMM)本身是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,通過規範定義了程序中的各個變量(包括實例字段、靜態字段和構成數組對象的元素)的訪問方式。
JMM的同步規定:
-
線程解鎖前,必須把共享變量的值刷新回主內存
-
線程加鎖前,必須讀取主內存的最新值到自己的工作內存
-
加鎖解鎖是同一把鎖
由於JVM運行程序的實體是線程,而每個線程創建時JVM都會爲其創建一個工作內存(有些地方稱爲棧空間),工作內存時每個線程的私有數據區域,而Java內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝到自己的工作內存空間,然後對變量進行操作,操作完成後再將變量寫回到主內存,不能直接操作主內存中的變量,各個線程的工作內存中存儲着主內存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成,其簡要的訪問過程如下圖:
JMM的三大特性
JMM是線程安全性獲得的保證。因爲JMM具有如下特點:
-
可見性:從主內存拷貝變量後,如果某一個線程在自己的工作內存中對變量進行了修改,然後寫回了主內存,其它線程能第一時間看到,這就叫作可見性。
-
原子性:不可分割,完整性,也即某個線程正在做某個具體業務時,中間不可以被加塞或者被分割
-
有序性:禁止指令重排,按照規定的順序去執行
綜上所述,volatile滿足JMM三大特性中的兩個,即可見性和有序性,volatile並不滿足原子性,所以說volatile是輕量級的同步機制。
3. 代碼驗證Volatile的可見性
代碼示例:
/**
* Created by salmonzhang on 2020/7/4.
* 可見性代碼實例
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in ...");
//暫停一會兒線程
try{ TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
myData.addTo10();
System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
},"Thread01").start();
while (myData.number == 0) {
//main線程一直在這裏等待,直到number的值不再等於零
}
System.out.println(Thread.currentThread().getName()+"\t mission is over , number updated ...");
}
}
class MyData{
// int number = 0; // 這裏沒有加volatile
volatile int number = 0; // 這裏加了volatile
public void addTo10() {
this.number = 10;
}
}
沒有加volatile的運行結果:
加了volatile的運行結果:
總結:如果不加volatile關鍵字,則主線程會進入死循環,加了volatile時主線程運行正常,可以正常退出,說明加了volatile關鍵字後,當有一個線程修改了變量的值,其它線程會在第一時間知道,當前值作廢,重新從主內存中獲取值。這種修改變量的值,讓其它線程第一時間知道,就叫作可見性。
4. 代碼驗證Volatile不保證原子性
代碼示例:
/**
* Created by salmonzhang on 2020/7/4.
* 驗證volatile不保證原子性
* 原子性是什麼意思:
* 不可分割,完整性,也即某個線程正在做某個具體業務時,中間不可以被加塞或者被分割。
* 需要整體完整,要麼同時成功,要麼同時失敗。保證數據的原子一致性
*/
public class VolatileDemo2 {
public static void main(String[] args) {
MyData2 myData2 = new MyData2();
for (int i = 1; i <= 20; i++){
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData2.addPlusPuls();
}
},String.valueOf(i)).start();
}
//需要等待上面20個線程全部執行完成後,再用main線程取得最終的結果值看看是多少?
while (Thread.activeCount() > 2) { //後臺默認有兩個線程:GC線程和main線程
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "finally number value = " + myData2.number);
}
}
class MyData2{
volatile int number = 0; // 這裏加了volatile
public void addPlusPuls() {
number++;
}
}
運行結果:
從代碼的運行結果會發現:會出現number最終的結果有可能出現不是20000的時候,這就證明了volatile不能保證原子性。
5. volatile不能保證原子性的原因和解決方案
-
爲什麼volatile不能保證原子性?
由於多線程進程調度的關係,在某一時間段出現了丟失寫值的情況。因爲線程切換太快,會出現後面的線程會把前面的線程的值剛好覆蓋。
例如:Thread1和Thread2同時從主內存中讀取number的值1到自己的工作內存,並同時進行了+1的動作,當Thread1將2寫會主內存的時候,由於線程的調度原因,Thread2並沒有第一時間知道Thread1已經將number的值改爲了2,而是直接將Thread1改的number值進行覆蓋,這樣就會導致數據丟失。
-
解決方案:
2.1. 直接在addPlusPuls前面加上synchronized
class MyData2{ volatile int number = 0; // 這裏加了volatile public synchronized void addPlusPuls() { number++; } }
但是爲了保證一個number++的原子性直接用synchronized,感覺有點重,類似於“殺雞用牛刀”
2.2 用atomic
class MyData2{ AtomicInteger number = new AtomicInteger(); public void addPlusPuls() { number.getAndIncrement(); } }
7. 有序性
-
計算機在執行程序時,爲了提高性能,編譯器的處理器通常會對指令做重排,一般有三種重排:
-
編譯器的重排
-
指令並行的重排
-
內存系統的重排
-
-
單線程環境裏確保程序最終執行的結果和代碼執行的結果一致
-
處理器在進行重排序時,必須考慮指令之間的數據依懶性
-
多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證用的變量能否一致性是無法確定的,結果也是無法預測的
重排案例一:
public void mySort(){
int x=11;//語句1
int y=12;//語句2
x=x+5;//語句3
y=x*x;//語句4
}
計算機執行的順序可能是:
1234
2134
1324
問題:
請問語句4可以重排後變成第一條碼?
存在數據的依賴性,所以沒辦法排到第一個
重排案例二:
指令重排代碼示例:
public class ReSortSeqDemo {
int a = 0;
boolean flag = false;
public void method01() {
a = 1; // 這裏的a和flag沒有禁止指令重排,所以在多線程環境中就有可能出現問題
flag = true;
}
public void method02() {
if (flag) {
a = a + 3;
System.out.println("a = " + a);
}
}
}
這裏的a和flag沒有禁止指令重排,所以在多線程環境中就有可能出現問題,例如指令重排後,method01中的flag=true先被Thread1執行了,此時Thread2又搶佔到了線程資源去執行method02()時,此時的運行結果就是有問題的。運行結果就是a = 3,而不是正常情況下的a = 4
7. 單例模式下可能存在線程不安全
代碼示例:
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的構造方法");
};
//synchronized 解決單例的多線程問題,會顯得比較重,整個方法都被鎖住了,不建議這麼寫
public static SingletonDemo getInstance(){
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
//併發多線程後,會出現構造函數多次執行的情況
for (int i = 1; i <= 10; i++){
new Thread(() -> {
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
運行結果:
8. 單例模式下的volatile分析
1.代碼示例:
public class SingletonDemo {
private static volatile SingletonDemo instance = null; //加上volatile,禁止編譯器指令重排
private SingletonDemo(){
System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的構造方法");
};
/**
* DCL (double check Lock 雙端檢索機制)
*/
public static SingletonDemo getInstance(){
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
//併發多線程後,會出現構造函數多次執行的情況
for (int i = 1; i <= 10; i++){
new Thread(() -> {
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
總結:
-
如果沒有加 volatile 就不一定是線程安全的,原因是指令重排序的存在,加入 volatile 可以禁止指令重排。
-
原因是在於某一個線程執行到第一次檢測,讀取到的 instance 不爲 null 時,instance 的引用對象可能還沒有完成初始化。
-
instance = new Singleton() 可以分爲以下三步完成
memory = allocate(); // 1.分配對象空間 instance(memory); // 2.初始化對象 instance = memory; // 3.設置instance指向剛分配的內存地址,此時instance != null
-
步驟 2 和步驟 3 不存在依賴關係,而且無論重排前還是重排後程序的執行結果在單線程中並沒有改變,因此這種優化是允許的。
-
發生重排
memory = allocate(); // 1.分配對象空間 instance = memory; //3.設置instance指向剛分配的內存地址,此時instance != null,但對象還沒有初始化完成 instance(memory); // 2.初始化對象
-
所以不加 volatile 返回的實例不爲空,但可能是未初始化的實例