volatile的学习总结
1.volatile是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制
-
保證可見性
-
不保證原子性
-
禁止指令重排
2. Java內(nèi)存模型(JMM)
JMM(Java內(nèi)存模型Java Memory Model,簡稱JMM)本身是一種抽象的概念,并不真實(shí)存在,它描述的是一組規(guī)則或規(guī)范,通過規(guī)范定義了程序中的各個(gè)變量(包括實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問方式。
JMM的同步規(guī)定:
線程解鎖前,必須把共享變量的值刷新回主內(nèi)存
線程加鎖前,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存
加鎖解鎖是同一把鎖
由于JVM運(yùn)行程序的實(shí)體是線程,而每個(gè)線程創(chuàng)建時(shí)JVM都會(huì)為其創(chuàng)建一個(gè)工作內(nèi)存(有些地方稱為棧空間),工作內(nèi)存時(shí)每個(gè)線程的私有數(shù)據(jù)區(qū)域,而Java內(nèi)存模型中規(guī)定所有變量都存儲(chǔ)在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問,但線程對(duì)變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行,首先要將變量從主內(nèi)存拷貝到自己的工作內(nèi)存空間,然后對(duì)變量進(jìn)行操作,操作完成后再將變量寫回到主內(nèi)存,不能直接操作主內(nèi)存中的變量,各個(gè)線程的工作內(nèi)存中存儲(chǔ)著主內(nèi)存中的變量副本拷貝,因此不同的線程間無法訪問對(duì)方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成,其簡要的訪問過程如下圖:
JMM的三大特性
JMM是線程安全性獲得的保證。因?yàn)镴MM具有如下特點(diǎn):
可見性:從主內(nèi)存拷貝變量后,如果某一個(gè)線程在自己的工作內(nèi)存中對(duì)變量進(jìn)行了修改,然后寫回了主內(nèi)存,其它線程能第一時(shí)間看到,這就叫作可見性。
原子性:不可分割,完整性,也即某個(gè)線程正在做某個(gè)具體業(yè)務(wù)時(shí),中間不可以被加塞或者被分割
有序性:禁止指令重排,按照規(guī)定的順序去執(zhí)行
綜上所述,volatile滿足JMM三大特性中的兩個(gè),即可見性和有序性,volatile并不滿足原子性,所以說volatile是輕量級(jí)的同步機(jī)制。
3. 代碼驗(yàn)證Volatile的可見性
代碼示例:
/*** Created by salmonzhang on 2020/7/4.* 可見性代碼實(shí)例*/public class VolatileDemo {public static void main(String[] args) {MyData myData = new MyData();new Thread(() -> {System.out.println(Thread.currentThread().getName() + "\t come in ...");//暫停一會(huì)兒線程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; // 這里沒有加volatilevolatile int number = 0; // 這里加了volatilepublic void addTo10() {this.number = 10;}}沒有加volatile的運(yùn)行結(jié)果:
加了volatile的運(yùn)行結(jié)果:
總結(jié):如果不加volatile關(guān)鍵字,則主線程會(huì)進(jìn)入死循環(huán),加了volatile時(shí)主線程運(yùn)行正常,可以正常退出,說明加了volatile關(guān)鍵字后,當(dāng)有一個(gè)線程修改了變量的值,其它線程會(huì)在第一時(shí)間知道,當(dāng)前值作廢,重新從主內(nèi)存中獲取值。這種修改變量的值,讓其它線程第一時(shí)間知道,就叫作可見性。
4. 代碼驗(yàn)證Volatile不保證原子性
代碼示例:
/*** Created by salmonzhang on 2020/7/4.* 驗(yàn)證volatile不保證原子性* 原子性是什么意思:* 不可分割,完整性,也即某個(gè)線程正在做某個(gè)具體業(yè)務(wù)時(shí),中間不可以被加塞或者被分割。* 需要整體完整,要么同時(shí)成功,要么同時(shí)失敗。保證數(shù)據(jù)的原子一致性*/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個(gè)線程全部執(zhí)行完成后,再用main線程取得最終的結(jié)果值看看是多少?while (Thread.activeCount() > 2) { //后臺(tái)默認(rèn)有兩個(gè)線程:GC線程和main線程Thread.yield();}System.out.println(Thread.currentThread().getName() + "finally number value = " + myData2.number);}}class MyData2{volatile int number = 0; // 這里加了volatilepublic void addPlusPuls() {number++;}}運(yùn)行結(jié)果:
從代碼的運(yùn)行結(jié)果會(huì)發(fā)現(xiàn):會(huì)出現(xiàn)number最終的結(jié)果有可能出現(xiàn)不是20000的時(shí)候,這就證明了volatile不能保證原子性。
5. volatile不能保證原子性的原因和解決方案
為什么volatile不能保證原子性?
由于多線程進(jìn)程調(diào)度的關(guān)系,在某一時(shí)間段出現(xiàn)了丟失寫值的情況。因?yàn)榫€程切換太快,會(huì)出現(xiàn)后面的線程會(huì)把前面的線程的值剛好覆蓋。
例如:Thread1和Thread2同時(shí)從主內(nèi)存中讀取number的值1到自己的工作內(nèi)存,并同時(shí)進(jìn)行了+1的動(dòng)作,當(dāng)Thread1將2寫會(huì)主內(nèi)存的時(shí)候,由于線程的調(diào)度原因,Thread2并沒有第一時(shí)間知道Thread1已經(jīng)將number的值改為了2,而是直接將Thread1改的number值進(jìn)行覆蓋,這樣就會(huì)導(dǎo)致數(shù)據(jù)丟失。
解決方案:
2.1. 直接在addPlusPuls前面加上synchronized
class MyData2{volatile int number = 0; // 這里加了volatilepublic synchronized void addPlusPuls() {number++;} }但是為了保證一個(gè)number++的原子性直接用synchronized,感覺有點(diǎn)重,類似于“殺雞用牛刀”
2.2 用atomic
class MyData2{AtomicInteger number = new AtomicInteger();public void addPlusPuls() {number.getAndIncrement();} }7. 有序性
計(jì)算機(jī)在執(zhí)行程序時(shí),為了提高性能,編譯器的處理器通常會(huì)對(duì)指令做重排,一般有三種重排:
-
編譯器的重排
-
指令并行的重排
-
內(nèi)存系統(tǒng)的重排
單線程環(huán)境里確保程序最終執(zhí)行的結(jié)果和代碼執(zhí)行的結(jié)果一致
處理器在進(jìn)行重排序時(shí),必須考慮指令之間的數(shù)據(jù)依懶性
多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在,兩個(gè)線程中使用的變量能否保證用的變量能否一致性是無法確定的,結(jié)果也是無法預(yù)測(cè)的
重排案例一:
public void mySort(){int x=11;//語句1int y=12;//語句2x=x+5;//語句3y=x*x;//語句4 }計(jì)算機(jī)執(zhí)行的順序可能是:
1234
2134
1324
問題:
請(qǐng)問語句4可以重排后變成第一條碼?
存在數(shù)據(jù)的依賴性,所以沒辦法排到第一個(gè)
重排案例二:
指令重排代碼示例:
public class ReSortSeqDemo {int a = 0;boolean flag = false;public void method01() {a = 1; // 這里的a和flag沒有禁止指令重排,所以在多線程環(huán)境中就有可能出現(xiàn)問題flag = true;}public void method02() {if (flag) {a = a + 3;System.out.println("a = " + a);}} }這里的a和flag沒有禁止指令重排,所以在多線程環(huán)境中就有可能出現(xiàn)問題,例如指令重排后,method01中的flag=true先被Thread1執(zhí)行了,此時(shí)Thread2又搶占到了線程資源去執(zhí)行method02()時(shí),此時(shí)的運(yùn)行結(jié)果就是有問題的。運(yùn)行結(jié)果就是a = 3,而不是正常情況下的a = 4
7. 單例模式下可能存在線程不安全
代碼示例:
public class SingletonDemo {private static SingletonDemo instance = null;private SingletonDemo(){System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的構(gòu)造方法");};//synchronized 解決單例的多線程問題,會(huì)顯得比較重,整個(gè)方法都被鎖住了,不建議這么寫public static SingletonDemo getInstance(){if (instance == null) {instance = new SingletonDemo();}return instance;}public static void main(String[] args) {//并發(fā)多線程后,會(huì)出現(xiàn)構(gòu)造函數(shù)多次執(zhí)行的情況for (int i = 1; i <= 10; i++){new Thread(() -> {SingletonDemo.getInstance();},String.valueOf(i)).start();}} }運(yùn)行結(jié)果:
8. 單例模式下的volatile分析
1.代碼示例:
public class SingletonDemo {private static volatile SingletonDemo instance = null; //加上volatile,禁止編譯器指令重排private SingletonDemo(){System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的構(gòu)造方法");};/*** DCL (double check Lock 雙端檢索機(jī)制)*/public static SingletonDemo getInstance(){if (instance == null) {synchronized (SingletonDemo.class) {if (instance == null) {instance = new SingletonDemo();}}}return instance;}public static void main(String[] args) {//并發(fā)多線程后,會(huì)出現(xiàn)構(gòu)造函數(shù)多次執(zhí)行的情況for (int i = 1; i <= 10; i++){new Thread(() -> {SingletonDemo.getInstance();},String.valueOf(i)).start();}} }總結(jié):
-
如果沒有加 volatile 就不一定是線程安全的,原因是指令重排序的存在,加入 volatile 可以禁止指令重排。
-
原因是在于某一個(gè)線程執(zhí)行到第一次檢測(cè),讀取到的 instance 不為 null 時(shí),instance 的引用對(duì)象可能還沒有完成初始化。
-
instance = new Singleton() 可以分為以下三步完成
memory = allocate(); // 1.分配對(duì)象空間 instance(memory); // 2.初始化對(duì)象instance = memory; // 3.設(shè)置instance指向剛分配的內(nèi)存地址,此時(shí)instance != null -
步驟 2 和步驟 3 不存在依賴關(guān)系,而且無論重排前還是重排后程序的執(zhí)行結(jié)果在單線程中并沒有改變,因此這種優(yōu)化是允許的。
-
發(fā)生重排
memory = allocate(); // 1.分配對(duì)象空間 instance = memory; //3.設(shè)置instance指向剛分配的內(nèi)存地址,此時(shí)instance != null,但對(duì)象還沒有初始化完成 instance(memory); // 2.初始化對(duì)象 -
所以不加 volatile 返回的實(shí)例不為空,但可能是未初始化的實(shí)例
非常感謝您的耐心閱讀,希望我的文章對(duì)您有幫助。歡迎點(diǎn)評(píng)、轉(zhuǎn)發(fā)或分享給您的朋友或技術(shù)群。
與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的volatile的学习总结的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构——顺序存储二叉树
- 下一篇: Visual Studio下载、安装、运