volatile有哪些應用場景?
點擊上方 Java學習之道,選擇 設(shè)為星標
來源: cnblogs.com/yuxueyyz/p/14972113.html
作者: 浴血
Part1對volatile的理解
volitale是Java虛擬機提供的一種輕量級的同步機制
三大特性:
保證可見性 不保證原子性 禁止指令重排
1首先保證可見性
1.1 可見性
概念:當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看到修改的值
package com.yuxue.juc.volatileTest;
/**
* 1驗證volatile的可見性
* 1.1 如果int num = 0,number變量沒有添加volatile關(guān)鍵字修飾
* 1.2 添加了volatile,可以解決可見性
*/
class VolatileDemo1 {
//自定義的類
public static class MyTest{
//類的內(nèi)部成員變量num
public int num = 0;
//numTo60 方法,讓num值為60
public void numTo60(){
num = 60;
}
}
public static void main(String[] args) {
MyTest myTest = new MyTest();
//第一個線程
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t come in");
Thread.sleep(3000);
myTest.numTo60();
System.out.println(Thread.currentThread().getName() + "\t update value:" + myTest.num);
} catch (InterruptedException e) {
e.printStackTrace();
}
} ,"thread1").start();;
//主線程判斷num值
while (myTest.num == 0){
//如果myData的num一直為零,main線程一直在這里循環(huán)
}
System.out.println(Thread.currentThread().getName() + "\t mission is over, num value is " + myTest.num);
}
}
如上代碼是沒有保證可見性的,可見性存在于JMM當中即java內(nèi)存模型當中的,可見性主要是指當一個線程改變其內(nèi)部的工作內(nèi)存當中的變量后,其他線程是否可以觀察到,因為不同的線程件無法訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成,因為此處沒有添加volatile指令,導致其中thread1對num值變量進行更改時,main線程無法感知到num值發(fā)生更改,導致在while處無限循環(huán),讀不到新的num值,會發(fā)生死循環(huán)
此時修改類中代碼為
/**
* volatile可以保證可見性,及時通知其他線程,主物理內(nèi)存的值已經(jīng)被修改
*/
public static class MyTest{
//類的內(nèi)部成員變量num
public volatile int num = 0;
//numTo60 方法,讓num值為60
public void numTo60(){
num = 60;
}
}
此時volatile就可以保證內(nèi)存的可見性,此時運行代碼就可以發(fā)現(xiàn)
1.2 不保證原子性
原子性概念:不可分割、完整性,即某個線程正在做某個具體業(yè)務時,中間不可以被加塞或者被分割,需要整體完整,要么同時成功,要么同時失敗 類代碼為:
//自定義的類
public static class MyTest {
//類的內(nèi)部成員變量num
public volatile int num = 0;
public void numPlusPlus() {
num++;
}
}
主方法為
public static void main(String[] args) {
MyTest myTest = new MyTest();
/**
* 10個線程創(chuàng)建出來,每個線程執(zhí)行2000次num++操作
* 我們知道,在字節(jié)碼及底層,i++被抽象為三個操作
* 即先取值,再自加,再賦值操作
*/
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
for (int j = 0; j < 2000; j++) {
myTest.numPlusPlus();
}
}, "Thread" + i).start();
}
//這里規(guī)定線程數(shù)大于2,一般有GC線程以及main主線程
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "\t finally num value is " + myTest.num);
}
代碼如上所示,如果volatile保證原子性,那么10個線程分別執(zhí)行自加2000次操作,那么最終結(jié)果一定是20000,但是執(zhí)行三次結(jié)果如下
//第一次
main finally num value is 19003
//第二次
main finally num value is 18694
//第三次
main finally num value is 19552
可以發(fā)現(xiàn),我們num的值每次都不相同,且最后的值都沒有達到20000,這是為什么呢?
為什么會出現(xiàn)這種情況?
首先,我們要考慮到這種情況,假如線程A執(zhí)行到第11行即myTest.numPlusPlus();方法時
線程進入方法執(zhí)行numPlusPlus方法后,num的值不管是多少,線程A將num的值首先初始化為0(假如主存中num的值為0),之后num的值自增為1,之后線程A掛起,線程B此時也將主存中的num值讀到自己的工作內(nèi)存中值為0,之后num的值自增1,之后線程B掛起,線程A繼續(xù)運行將num的值寫回主存,但是因為volatile關(guān)鍵字保證可見性,但是在很短的時間內(nèi),線程B也將num的值寫回主存,此時num的值就少加了一次,所以最后總數(shù)基本上少于20000
如何解決?
但是JUC有線程的原子類為AtomicInteger類,此時,將類代碼更改為
public static class MyTest {
//類的內(nèi)部成員變量num
public volatile int num = 0;
AtomicInteger atomicInteger = new AtomicInteger();
//numTo60 方法,讓num值為60
public void numTo60() {
num = 60;
}
public void numPlusPlus() {
num++;
}
public void myAtomPlus(){
atomicInteger.getAndIncrement();
}
}
共同測試num和atomicInteger,此時執(zhí)行主函數(shù),三次結(jié)果為
//第一次
main finally num value is 19217
main finally atomicInteger value is 20000
//第二次
main finally num value is 19605
main finally atomicInteger value is 20000
//第三次
main finally num value is 18614
main finally atomicInteger value is 20000
我們發(fā)現(xiàn)volatile關(guān)鍵字并沒有保證我們的變量的原子性,但是JUC內(nèi)部的AtomicInteger類保證了我們變量相關(guān)的原子性,AtomicInteger底層用到了CAS,CAS
1.3 禁止指令重排
有序性的概念:在計算機執(zhí)行程序時,為了提高性能,編譯器和處理器常常會對指令做重排。
一般分以下三種:
單線程環(huán)境里面確保程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致。 處理器在進行重排順序是必須要考慮指令之間的數(shù)據(jù)依賴性 多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在,兩個線程中使用的變量能否保證一致性時無法確定的,結(jié)果無法預測 重排代碼實例:聲明變量: int a,b,x,y=0
| 線程A | 線程B |
|---|---|
| x=a; | y=b; |
| b=1; | a=2; |
| 執(zhí)行結(jié)果 | x=0,y=0 |
如果編譯器對這段程序代碼執(zhí)行重排優(yōu)化后,可能出現(xiàn)如下情況:
| 線程A | 線程B |
|---|---|
| b=1; | a=2; |
| x=a; | y=b; |
| 執(zhí)行結(jié)果 | x=2,y=1 |
這個結(jié)果說明在多線程環(huán)境下,由于編譯器優(yōu)化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的
volatile實現(xiàn)禁止指令重排,從而避免了多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象
Part2內(nèi)存屏障
內(nèi)存屏障(Memory Barrier) 又稱內(nèi)存柵欄,是一個CPU指令。
他的作用有兩個:
保證特定操作的執(zhí)行順序 保證某些變量的內(nèi)存可見性(利用該特性實現(xiàn)volatile的內(nèi)存可見性)
由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在之間插入一條Memory Barrier則會告訴編譯器和CPU, 不管什么指令都不能和這條Memory Barrier指令重排順序,也就是說通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化。 內(nèi)存屏障另外一個作用是強制刷出各種CPU的緩存數(shù)據(jù),因此任何CPU上的線程都能讀 取到這些數(shù)據(jù)的最新版本
Part3JMM(java內(nèi)存模型)
為什么提到JMM?JMM當中規(guī)定了可見性、原子性、以及有序性的問題,在多線程中只要保證了以上問題的正確性,那么基本上不會發(fā)生多線程當中存在數(shù)據(jù)安全問題
JMM(Java Memory Model)本身是一種抽象的概念,并不真實存在,他描述的時一組規(guī)則或規(guī)范,通過這組規(guī)范定義了程序中各個變量(包括實例字段,靜態(tài)字段和構(gòu)成數(shù)組對象的元素)的訪問方式。
JMM關(guān)于同步的規(guī)定:
線程解鎖前,必須把共享變量的值刷新回主內(nèi)存 線程加鎖前,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存 加鎖解鎖時同一把鎖
由于JVM運行程序的實體是線程,而每個線程創(chuàng)建時JVM都會為其創(chuàng)建一個工作內(nèi)存(有的成為??臻g),工作內(nèi)存是每個線程的私有數(shù)據(jù)區(qū)域,而java內(nèi)存模型中規(guī)定所有變量都存儲在主內(nèi)存,主內(nèi)存是貢獻內(nèi)存區(qū)域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內(nèi)存中進行,首先概要將變量從主內(nèi)存拷貝到自己的工作內(nèi)存空間,然后對變量進行操作,操作完成后再將變量寫回主內(nèi)存,不能直接操作主內(nèi)存中的變量,各個線程中的工作內(nèi)存中存儲著主內(nèi)存的變量副本拷貝,因此不同的線程件無法訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成
期間要訪問過程如下圖:
JMM的三大特性
可見性
原子性
有序性 所以JMM當中的2.1和2.3在volatile當中都有很好的體現(xiàn),volatile關(guān)鍵字并不能保證多線程當中的原子性,但是volatile是輕量級的同步機制,不想synchronized鎖一樣粒度太大
Part4哪些地方用過volatile
當普通單例模式在多線程情況下:
/**
* 普通單例模式
* */
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 構(gòu)造方法 SingletonDemo()");
}
public static SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
//構(gòu)造方法只會被執(zhí)行一次
// System.out.println(getInstance() == getInstance());
// System.out.println(getInstance() == getInstance());
// System.out.println(getInstance() == getInstance());
//并發(fā)多線程后,構(gòu)造方法會在一些情況下執(zhí)行多次
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, "Thread " + i).start();
}
}
}
此時會出現(xiàn)兩個線程運行了SingletonDemo的構(gòu)造方法
此時就違反了單例模式的規(guī)定,其構(gòu)造方法在一些情況下會被執(zhí)行多次
2解決方式:
單例模式DCL代碼
DCL (Double Check Lock雙端檢鎖機制)在加鎖前和加鎖后都進行一次判斷
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
不僅兩次判空讓程序執(zhí)行更有效率,同時對代碼塊加鎖,保證了線程的安全性
但是!還存在問題!什么問題?
》大部分運行結(jié)果構(gòu)造方法只會被執(zhí)行一次,但指令重排機制會讓程序很小的幾率出現(xiàn)構(gòu)造方法被執(zhí)行多次
DCL(雙端檢鎖)機制不一定線程安全,原因時有指令重排的存在,加入volatile可以禁止指令重排
原因是在某一個線程執(zhí)行到第一次檢測,讀取到instance不為null時,instance的引用對象可能沒有完成初始化。instance=new SingleDemo();可以被分為一下三步(偽代碼):
memory = allocate();//1.分配對象內(nèi)存空間
instance(memory); //2.初始化對象
instance = memory; //3.設(shè)置instance執(zhí)行剛分配的內(nèi)存地址,此時instance!=null
步驟2和步驟3不存在數(shù)據(jù)依賴關(guān)系,而且無論重排前還是重排后程序的執(zhí)行結(jié)果在單線程中并沒有改變,因此這種重排優(yōu)化時允許的
所以如果3步驟提前于步驟2,但是instance還沒有初始化完成指令重排只會保證串行語義的執(zhí)行的一致性(單線程),但并不關(guān)心多線程間的語義一致性。
所以當一條線程訪問instance不為null時,由于instance示例未必已初始化完成,也就造成了線程安全問題。
此時加上volatile后就不會出現(xiàn)線程安全問題
private static volatile SingletonDemo instance = null;
因為volatile禁止了指令重排序的問題
-
| 更多精彩文章 -
▽加我微信,交個朋友 長按/掃碼添加↑↑↑



