java 多線程與并發(fā)之volatile詳解分析
CPU、內(nèi)存、緩存的關(guān)系
要理解JMM,要先從計(jì)算機(jī)底層開始,下面是一份大佬的研究報(bào)告

計(jì)算機(jī)在做一些我們平時(shí)的基本操作時(shí),需要的響應(yīng)時(shí)間是不一樣的!如果我們計(jì)算一次a+b所需要的的時(shí)間:
- CPU讀取內(nèi)存獲得a,100納秒
- CPU讀取內(nèi)存獲得b,100納秒
- CPU執(zhí)行一條指令 a+b ,0.6納秒
也就是說99%的時(shí)間花費(fèi)在CPU讀取內(nèi)存上了,那如何解決速度不均衡問題?
早期計(jì)算機(jī)中cpu和內(nèi)存的速度是差不多的,但在現(xiàn)代計(jì)算機(jī)中cpu的指令速度遠(yuǎn)超內(nèi)存的存取速度,由于計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算速度有幾個(gè)數(shù)量級(jí)的差距,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運(yùn)算速度的高速緩存(Cache)來作為內(nèi)存與處理器之間的緩沖:將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱交貎?nèi)存之中,這樣處理器就無(wú)須等待緩慢的內(nèi)存讀寫了
CPU緩存
什么是CPU緩存
在計(jì)算機(jī)系統(tǒng)中,CPU高速緩存(英語(yǔ):CPU Cache,在本文中簡(jiǎn)稱緩存)是用于減少處理器訪問內(nèi)存所需平均時(shí)間的部件。在金字塔式存儲(chǔ)體系中它位于自頂向下的第二層,僅次于CPU寄存器。其容量遠(yuǎn)小于內(nèi)存,但速度卻可以接近處理器的頻率。當(dāng)處理器發(fā)出內(nèi)存訪問請(qǐng)求時(shí),會(huì)先查看緩存內(nèi)是否有請(qǐng)求數(shù)據(jù)。如果存在(命中),則不經(jīng)訪問內(nèi)存直接返回該數(shù)據(jù);如果不存在(失效),則要先把內(nèi)存中的相應(yīng)數(shù)據(jù)載入緩存,再將其返回處理器。
下圖是一個(gè)典型的存儲(chǔ)器層次結(jié)構(gòu),我們可以看到一共使用了三級(jí)緩存:

為什么要有多級(jí)CPU Cache
在計(jì)算機(jī)系統(tǒng)中,寄存器劃是L0級(jí)緩存,接著依次是L1,L2,L3(接下來是內(nèi)存,本地磁盤,遠(yuǎn)程存儲(chǔ))。越往上的緩存存儲(chǔ)空間越小,速度越快,成本也更高;越往下的存儲(chǔ)空間越大,速度更慢,成本也更低。從上至下,每一層都可以看做是更下一層的緩存,即:L0寄存器是L1一級(jí)緩存的緩存,L1是L2的緩存,依次類推;每一層的數(shù)據(jù)都是來至它的下一層,所以每一層的數(shù)據(jù)是下一層的數(shù)據(jù)的子集

下圖是我電腦的三級(jí)緩存,可以看到層級(jí)越小容量越小。速度越快價(jià)格越高??!

在現(xiàn)代CPU上,一般來說L0, L1,L2,L3都集成在CPU內(nèi)部,而L1還分為一級(jí)數(shù)據(jù)緩存(Data Cache,D-Cache,L1d)和一級(jí)指令緩存(Instruction Cache,I-Cache,L1i),分別用于存放數(shù)據(jù)和執(zhí)行數(shù)據(jù)的指令解碼。每個(gè)核心擁有獨(dú)立的運(yùn)算處理單元、控制器、寄存器、L1、L2緩存,然后一個(gè)CPU的多個(gè)核心共享最后一層CPU緩存L3。
為了充分利用 CPU Cache,Java提出了內(nèi)存模型這個(gè)概念
Java內(nèi)存模型(Java Memory Model,JMM)
從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(Main Memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。

程之間的共享變量存儲(chǔ)在主內(nèi)存(Main Memory)中,每個(gè)線程都有一個(gè)私有的工作內(nèi)存(Local Memory),工作內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的副本。

舉個(gè)栗子:多個(gè)線程去修改主內(nèi)存中的變量a。線程不能直接修改主內(nèi)存中的數(shù)據(jù),先把數(shù)據(jù)拷貝到工作內(nèi)存,線程對(duì)私有的工作內(nèi)存修改然后再同步到主內(nèi)存。那這樣做會(huì)帶來什么問題呢?
JMM導(dǎo)致的并發(fā)安全問題
從JMM角度看,如果兩個(gè)線程同時(shí)調(diào)用 a=a+1這個(gè)函數(shù)(假設(shè)a的初始值是0),A、B線程同時(shí)從主內(nèi)存中拷貝a=0,然后修改寫回,最后主內(nèi)存為a=1,咋搞?

如下是代碼栗子
public class MainTest {
private long count = 0;
public void incCount() {
count += 1;
}
public static void main(String[] args) throws InterruptedException {
MainTest test = new MainTest();
Count count = new Count(test);
Count count1 = new Count(test);
count.start();
count1.start();
Thread.sleep(5);
System.out.println("result is :" + test.count);
}
private static class Count extends Thread{
private MainTest m;
public Count(MainTest m){
this.m = m;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
m.incCount();
}
}
}
}
執(zhí)行結(jié)果
// 第一次執(zhí)行
> Task :lib-test:MainTest.main()
result is :11861// 第二次執(zhí)行
> Task :lib-test:MainTest.main()
result is :10535
可見性
可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值
由于線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量,那么對(duì)于共享變量a,它們首先是在自己的工作內(nèi)存,之后再同步到主內(nèi)存。可是并不會(huì)及時(shí)的刷到主存中,而是會(huì)有一定時(shí)間差。很明顯,這個(gè)時(shí)候線程 A 對(duì)變量 a 的操作對(duì)于線程 B 而言就不具備可見性了 。
要解決共享對(duì)象可見性這個(gè)問題,我們可以使用volatile關(guān)鍵字或者是加鎖
原子性
即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過程不會(huì)被任何因素打斷,要么就都不執(zhí)行
我們都知道CPU資源的分配都是以線程為單位的,并且是分時(shí)調(diào)用,操作系統(tǒng)允許某個(gè)進(jìn)程執(zhí)行一小段時(shí)間,例如 50 毫秒,過了 50 毫秒操作系統(tǒng)就會(huì)重新選擇一個(gè)進(jìn)程來執(zhí)行(我們稱為“任務(wù)切換”),這個(gè) 50 毫秒稱為“時(shí)間片”。而任務(wù)的切換大多數(shù)是在時(shí)間片段結(jié)束以后。
那么線程切換為什么會(huì)帶來bug呢?因?yàn)椴僮飨到y(tǒng)做任務(wù)切換,可以發(fā)生在任何一條CPU 指令執(zhí)行完!注意,是 CPU 指令,CPU 指令,CPU 指令,而不是高級(jí)語(yǔ)言里的一條語(yǔ)句。比如count++,在java里就是一句話,但高級(jí)語(yǔ)言里一條語(yǔ)句往往需要多條 CPU 指令完成。其實(shí)count++包含了三個(gè)CPU指令
有序性
即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
在Java內(nèi)存模型中,為了效率是允許編譯器和處理器對(duì)指令進(jìn)行重排序,當(dāng)然重排序不會(huì)影響單線程的運(yùn)行結(jié)果,但是對(duì)多線程會(huì)有影響。Java提供volatile來保證一定的有序性。最著名的例子就是單例模式里面的DCL(雙重檢查鎖)。另外,可以通過synchronized和Lock來保證有序性,synchronized和Lock保證每個(gè)時(shí)刻是有一個(gè)線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。
在單線程的情況下,CPU執(zhí)行語(yǔ)句并不是按照順序來的,為了更高的執(zhí)行效率可能會(huì)重新排序,單線程下是可以提高執(zhí)行效率且保證正確。但在多線程下反而變成了安全問題,Java提供volatile來保證一定的有序性。此處不做深入!
volatile
volatile特性
- 可見性:對(duì)一個(gè)volatile變量的讀,總是能看到(任意線程)對(duì)這個(gè)volatile變量最后的寫入
- 原子性:對(duì)任意單個(gè)volatile變量的讀/寫具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性
【面試題】為什么volatile不能保證a++的線程安全問題
:線程執(zhí)行a++要經(jīng)歷讀取主內(nèi)存-加載-使用-賦值-寫內(nèi)存-寫回主內(nèi)存幾個(gè)階段,而且a++不是原子操作,至少可以分為三步執(zhí)行。線程A、B同時(shí)從主內(nèi)存讀取a的值,A線程執(zhí)行到加載階段切換上下文交出CPU使用權(quán),B線程完成整個(gè)操作并刷新了主內(nèi)存中a的值。此時(shí)A線程繼續(xù)賦值等其他操作,已經(jīng)造成了安全問題。可見性是保證線程每次讀取時(shí)必須讀取主內(nèi)存的值,對(duì)后續(xù)的操作沒有限制,不會(huì)因?yàn)橹鲀?nèi)存中的值改變而中斷了操作。如果是原子性則可以,synchronized可以保證原子性。

volatile 的實(shí)現(xiàn)原理
有volatile修飾的共享變量進(jìn)行寫操作的時(shí)候會(huì)使用CPU提供的Lock前綴指令
- 將當(dāng)前處理器緩存的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
- 這個(gè)寫回內(nèi)存的操作會(huì)使其他CPU里緩存了該地址的數(shù)據(jù)無(wú)效
單例模式的雙重鎖為什么要加volatile
public class TestInstance{
private volatile static TestInstance instance;
public static TestInstance getInstance(){ //1
if(instance == null){ //2
synchronized(TestInstance.class){ //3
if(instance == null){ //4
instance = new TestInstance(); //5
}
}
}
return instance; //6
}
}
需要volatile關(guān)鍵字的原因是,在并發(fā)情況下,如果沒有volatile關(guān)鍵字,在第5行會(huì)出現(xiàn)問題。
instance = new TestInstance()可以分解為3行偽代碼
a. memory = allocate() //分配內(nèi)存 b. ctorInstanc(memory) //初始化對(duì)象 c. instance = memory //設(shè)置instance指向剛分配的地址
上面的代碼在編譯運(yùn)行時(shí),可能會(huì)出現(xiàn)重排序從a-b-c排序?yàn)閍-c-b。在多線程的情況下會(huì)出現(xiàn)以下問題。當(dāng)線程A在執(zhí)行第5行代碼時(shí),B線程進(jìn)來執(zhí)行到第2行代碼。假設(shè)此時(shí)A執(zhí)行的過程中發(fā)生了指令重排序,即先執(zhí)行了a和c,沒有執(zhí)行b。那么由于A線程執(zhí)行了c導(dǎo)致instance指向了一段地址,所以B線程判斷instance不為null,會(huì)直接跳到第6行并返回一個(gè)未初始化的對(duì)象
總結(jié)
因?yàn)镃PU與內(nèi)存的速度差距越來越大,為了彌補(bǔ)速度差距引入了CPU緩存,又因?yàn)榫彺鎸?dǎo)致線程安全問題,從前到后縷出一條線來就很容易理解了。如果只是單線程完全不擔(dān)心什么指令重排,想要更高的執(zhí)行效率必然付出安全風(fēng)險(xiǎn)。知其然,知其所以然!
到此這篇關(guān)于java 多線程與并發(fā)之volatile詳解分析的文章就介紹到這了,更多相關(guān)Java volatile內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
實(shí)例講解String Date Calendar之間的轉(zhuǎn)換
下面小編就為大家?guī)硪黄獙?shí)例講解String Date Calendar之間的轉(zhuǎn)換。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07
Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之校園一卡通系統(tǒng)的實(shí)現(xiàn)
這是一個(gè)使用了java+Springboot+Maven+mybatis+Vue+mysql+wd開發(fā)的校園一卡通系統(tǒng),是一個(gè)畢業(yè)設(shè)計(jì)的實(shí)戰(zhàn)練習(xí),具有校園一卡通系統(tǒng)該有的所有功能,感興趣的朋友快來看看吧2022-01-01
Hikari?數(shù)據(jù)庫(kù)連接池內(nèi)部源碼實(shí)現(xiàn)的小細(xì)節(jié)
這篇文章主要介紹了Hikari?數(shù)據(jù)庫(kù)連接池內(nèi)部源碼實(shí)現(xiàn)的小細(xì)節(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02
springboot2+es7使用RestHighLevelClient的示例代碼
本文主要介紹了springboot2+es7使用RestHighLevelClient的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
Java實(shí)現(xiàn)學(xué)生成績(jī)輸出到磁盤文件的方法詳解
這篇文章主要為大家詳細(xì)介紹了如何利用Java實(shí)現(xiàn)將學(xué)生成績(jī)輸出到磁盤文件的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-11-11
一文了解Java Log框架徹底搞懂Log4J,Log4J2,LogBack,SLF4J
本文主要介紹了一文了解Java Log框架徹底搞懂Log4J,Log4J2,LogBack,SLF4J,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03
JAVA實(shí)現(xiàn)漢字轉(zhuǎn)拼音功能代碼實(shí)例
這篇文章主要介紹了JAVA實(shí)現(xiàn)漢字轉(zhuǎn)拼音功能代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05
SpringBoot集成tika實(shí)現(xiàn)word轉(zhuǎn)html的操作代碼
Tika是一個(gè)內(nèi)容分析工具,自帶全面的parser工具類,能解析基本所有常見格式的文件,得到文件的metadata,content等內(nèi)容,返回格式化信息,本文給大家介紹了SpringBoot集成tika實(shí)現(xiàn)word轉(zhuǎn)html的操作,需要的朋友可以參考下2024-06-06

