目錄:
1.volatile是什么
2.volatile的作用
3.volatile的原理
4.volatile與synchronized有什么區(qū)別
5.并發(fā)編程中的三個(gè)概念
6.使用volatile關(guān)鍵字的場景
7.CPU緩存的相關(guān)知識(shí)
8.學(xué)習(xí)成果檢查
9.擴(kuò)展-# Java 中 static 和 volatile 關(guān)鍵字的區(qū)別
10.擴(kuò)展閱讀
1.volatile是什么
volatile是Java提供的一種輕量級(jí)的同步機(jī)制,在并發(fā)編程中,它也扮演著比較重要的角色。同synchronized相比(synchronized通常稱為重量級(jí)鎖),volatile更輕量級(jí),相比使用synchronized所帶來的龐大開銷,倘若能恰當(dāng)?shù)暮侠淼氖褂胿olatile,自然是美事一樁。
Java語言規(guī)范對(duì)volatile的定義如下:
Java編程語言允許線程訪問共享變量,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過排他鎖單獨(dú)獲得這個(gè)變量。
上面比較繞口,通俗點(diǎn)講就是說一個(gè)變量如果用volatile修飾了,則Java可以確保所有線程看到這個(gè)變量的值是一致的,如果某個(gè)線程對(duì)volatile修飾的共享變量進(jìn)行更新,那么其他線程可以立馬看到這個(gè)更新,這就是所謂的線程可見性。
2.volatile的作用
2.1.volatile保證可見性
一旦一個(gè)共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
1)保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某個(gè)變量的值,這新值對(duì)其他線程來說是立即可見的。
2)禁止進(jìn)行指令重排序。
2.2.volatile不能確保原子性
原因:自增操作不是原子性操作,而且volatile也無法保證對(duì)變量的任何操作都是原子性的。
解決方案:可以通過synchronized或lock,進(jìn)行加鎖,來保證操作的原子性。也可以通過AtomicInteger。
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作類,即對(duì)基本數(shù)據(jù)類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個(gè)數(shù)),減法操作(減一個(gè)數(shù))進(jìn)行了封裝,保證這些操作是原子性操作。atomic是利用CAS來實(shí)現(xiàn)原子性操作的(Compare And Swap),CAS實(shí)際上是利用處理器提供的CMPXCHG指令實(shí)現(xiàn)的,而處理器執(zhí)行CMPXCHG指令是一個(gè)原子性操作。
2.3.volatile保證有序性
在前面提到volatile關(guān)鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。
volatile關(guān)鍵字禁止指令重排序有兩層意思:
1)當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見;在其后面的操作肯定還沒有進(jìn)行;
2)在進(jìn)行指令優(yōu)化時(shí),不能將在對(duì)volatile變量的讀操作或者寫操作的語句放在其后面執(zhí)行,也不能把volatile變量后面的語句放到其前面執(zhí)行。
3.volatile的原理
3.1.可見性
處理器為了提高處理速度,不直接和內(nèi)存進(jìn)行通訊,而是將系統(tǒng)內(nèi)存的數(shù)據(jù)獨(dú)到內(nèi)部緩存后再進(jìn)行操作,但操作完后不知什么時(shí)候會(huì)寫到內(nèi)存。
如果對(duì)聲明了volatile變量進(jìn)行寫操作時(shí),JVM會(huì)向處理器發(fā)送一條Lock前綴的指令,將這個(gè)變量所在緩存行的數(shù)據(jù)寫會(huì)到系統(tǒng)內(nèi)存。 這一步確保了如果有其他線程對(duì)聲明了volatile變量進(jìn)行修改,則立即更新主內(nèi)存中數(shù)據(jù)。
但這時(shí)候其他處理器的緩存還是舊的,所以在多處理器環(huán)境下,為了保證各個(gè)處理器緩存一致,每個(gè)處理會(huì)通過嗅探在總線上傳播的數(shù)據(jù)來檢查 自己的緩存是否過期,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改了,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài),當(dāng)處理器要對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作時(shí),會(huì)強(qiáng)制重新從系統(tǒng)內(nèi)存把數(shù)據(jù)讀到處理器緩存里。 這一步確保了其他線程獲得的聲明了volatile變量都是從主內(nèi)存中獲取最新的。
指令重排:指令重排序是JVM為了優(yōu)化指令,提高程序運(yùn)行效率,在不影響單線程程序執(zhí)行結(jié)果的前提下,盡可能地提高并行度。編譯器、處理器也遵循這樣一個(gè)目標(biāo)。注意是單線程。多線程的情況下指令重排序就會(huì)給程序員帶來問題。
3.2.有序性
Lock前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障(也成內(nèi)存柵欄),它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成。
3.3 lock前綴
下面這段話摘自《深入理解Java虛擬機(jī)》:
“觀察加入volatile關(guān)鍵字和沒有加入volatile關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn),加入volatile關(guān)鍵字時(shí),會(huì)多出一個(gè)lock前綴指令”
lock前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障(也成內(nèi)存柵欄),內(nèi)存屏障會(huì)提供3個(gè)功能:
1)它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成;(有序性)
2)它會(huì)強(qiáng)制將對(duì)緩存的修改操作立即寫入主存;(可見性)
3)如果是寫操作,它會(huì)導(dǎo)致其他CPU中對(duì)應(yīng)的緩存行無效。(可見性)
4.volatile與synchronized有什么區(qū)別
| volatile | synchronized |
|---|---|
| volatile是一個(gè)變量修飾符 | synchronized是一個(gè)方法或塊的修飾符 |
| volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀取 | synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住 |
| volatile僅能使用在變量級(jí)別 | synchronized則可以使用在變量、方法、和類級(jí)別的 |
| volatile僅能實(shí)現(xiàn)變量的修改可見性,不能保證原子性 | 而synchronized則可以保證變量的修改可見性和原子性 |
| volatile不會(huì)造成線程的阻塞 | synchronized可能會(huì)造成線程的阻塞 |
| volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化 | synchronized標(biāo)記的變量可以被編譯器優(yōu)化 |
5.并發(fā)編程中的三個(gè)概念
在并發(fā)編程中,我們通常會(huì)遇到以下三個(gè)問題:原子性問題,可見性問題,有序性問題。我們先看具體看一下這三個(gè)概念:
5.1.原子性
原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過程不會(huì)被任何因素打斷,要么就都不執(zhí)行。
范例1-銀行賬戶轉(zhuǎn)賬問題:
比如從賬戶A向賬戶B轉(zhuǎn)1000元,那么必然包括2個(gè)操作:從賬戶A減去1000元,往賬戶B加上1000元。
試想一下,如果這2個(gè)操作不具備原子性,會(huì)造成什么樣的后果。假如從賬戶A減去1000元之后,操作突然中止。
然后又從B取出了500元,取出500元之后,再執(zhí)行 往賬戶B加上1000元 的操作。
這樣就會(huì)導(dǎo)致賬戶A雖然減去了1000元,但是賬戶B沒有收到這個(gè)轉(zhuǎn)過來的1000元。
所以這2個(gè)操作必須要具備原子性才能保證不出現(xiàn)一些意外的問題。
同樣地反映到并發(fā)編程中會(huì)出現(xiàn)什么結(jié)果呢?
范例2:32位的變量賦值過程:
i = 9
假若一個(gè)線程執(zhí)行到這個(gè)語句時(shí),我暫且假設(shè)為一個(gè)32位的變量賦值包括兩個(gè)過程:為低16位賦值,為高16位賦值。
那么就可能發(fā)生一種情況:當(dāng)將低16位數(shù)值寫入之后,突然被中斷,而此時(shí)又有一個(gè)線程去讀取i的值,那么讀取到的就是錯(cuò)誤的數(shù)據(jù)。
5.2.可見性
可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
舉個(gè)簡單的例子,看下面這段代碼:
//線程1執(zhí)行的代碼
int i = 0;
i = 10;
//線程2執(zhí)行的代碼
j = i;
假若執(zhí)行線程1的是CPU1,執(zhí)行線程2的是CPU2。由上面的分析可知,當(dāng)線程1執(zhí)行 i =10這句時(shí),會(huì)先把i的初始值加載到CPU1的高速緩存中,然后賦值為10,那么在CPU1的高速緩存當(dāng)中i的值變?yōu)?0了,卻沒有立即寫入到主存當(dāng)中。
此時(shí)線程2執(zhí)行 j = i,它會(huì)先去主存讀取i的值并加載到CPU2的緩存當(dāng)中,注意此時(shí)內(nèi)存當(dāng)中i的值還是0,那么就會(huì)使得j的值為0,而不是10.
這就是可見性問題,線程1對(duì)變量i修改了之后,線程2沒有立即看到線程1修改的值。
5.3.有序性
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。舉個(gè)簡單的例子,看下面這段代碼:
int i = 0;
boolean flag = false;
i = 1; //語句1
flag = true; //語句2
上面代碼定義了一個(gè)int型變量,定義了一個(gè)boolean類型變量,然后分別對(duì)兩個(gè)變量進(jìn)行賦值操作。從代碼順序上看,語句1是在語句2前面的,那么JVM在真正執(zhí)行這段代碼的時(shí)候會(huì)保證語句1一定會(huì)在語句2前面執(zhí)行嗎?不一定,為什么呢?這里可能會(huì)發(fā)生指令重排序(Instruction Reorder)。
下面解釋一下什么是指令重排序,一般來說,處理器為了提高程序運(yùn)行效率,可能會(huì)對(duì)輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語句的執(zhí)行先后順序同代碼中的順序一致,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。
比如上面的代碼中,語句1和語句2誰先執(zhí)行對(duì)最終的程序結(jié)果并沒有影響,那么就有可能在執(zhí)行過程中,語句2先執(zhí)行而語句1后執(zhí)行。
但是要注意,雖然處理器會(huì)對(duì)指令進(jìn)行重排序,但是它會(huì)保證程序最終結(jié)果會(huì)和代碼順序執(zhí)行結(jié)果相同,那么它靠什么保證的呢?再看下面一個(gè)例子1:
int a = 10; //語句1
int r = 2; //語句2
a = a + 3; //語句3
r = a*a; //語句4
這段代碼有4個(gè)語句,那么可能的一個(gè)執(zhí)行順序是:
語句2->語句1->語句3->語句4
那么可不可能是這個(gè)執(zhí)行順序呢:
語句2->語句1->語句4->語句3
不可能,因?yàn)樘幚砥髟谶M(jìn)行重排序時(shí)是會(huì)考慮指令之間的數(shù)據(jù)依賴性,如果一個(gè)指令I(lǐng)nstruction 2必須用到Instruction 1的結(jié)果,那么處理器會(huì)保證Instruction 1會(huì)在Instruction 2之前執(zhí)行。
雖然重排序不會(huì)影響單個(gè)線程內(nèi)程序執(zhí)行的結(jié)果,但是多線程呢?下面看一個(gè)例子2:
//線程1:
context = loadContext(); //語句1
inited = true; //語句2
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代碼中,由于語句1和語句2沒有數(shù)據(jù)依賴性,因此可能會(huì)被重排序。假如發(fā)生了重排序,在線程1執(zhí)行過程中先執(zhí)行語句2,而此是線程2會(huì)以為初始化工作已經(jīng)完成,那么就會(huì)跳出while循環(huán),去執(zhí)行doSomethingwithconfig(context)方法,而此時(shí)context并沒有被初始化,就會(huì)導(dǎo)致程序出錯(cuò)。
從上面可以看出,指令重排序不會(huì)影響單個(gè)線程的執(zhí)行,但是會(huì)影響到線程并發(fā)執(zhí)行的正確性。
也就是說,要想并發(fā)程序正確地執(zhí)行,必須要保證原子性、可見性以及有序性。只要有一個(gè)沒有被保證,就有可能會(huì)導(dǎo)致程序運(yùn)行不正確。
6.使用volatile關(guān)鍵字的場景
synchronized關(guān)鍵字是防止多個(gè)線程同時(shí)執(zhí)行一段代碼,那么就會(huì)很影響程序執(zhí)行效率,而volatile關(guān)鍵字在某些情況下性能要優(yōu)于synchronized,但是要注意volatile關(guān)鍵字是無法替代synchronized關(guān)鍵字的,因?yàn)関olatile關(guān)鍵字無法保證操作的原子性。通常來說,使用volatile必須具備以下2個(gè)條件:
1)對(duì)變量的寫操作不依賴于當(dāng)前值
2)該變量沒有包含在具有其他變量的不變式中
實(shí)際上,這些條件表明,可以被寫入 volatile 變量的這些有效值獨(dú)立于任何程序的狀態(tài),包括變量的當(dāng)前狀態(tài)。
事實(shí)上,我的理解就是上面的2個(gè)條件需要保證操作是原子性操作,才能保證使用volatile關(guān)鍵字的程序在并發(fā)時(shí)能夠正確執(zhí)行。
下面列舉幾個(gè)Java中使用volatile的幾個(gè)場景。
6.1.狀態(tài)標(biāo)記量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
這段代碼是很典型的一段代碼,很多人在中斷線程時(shí)可能都會(huì)采用這種標(biāo)記辦法。但是事實(shí)上,這段代碼會(huì)完全運(yùn)行正確么?即一定會(huì)將線程中斷么?不一定,也許在大多數(shù)時(shí)候,這個(gè)代碼能夠把線程中斷,但是也有可能會(huì)導(dǎo)致無法中斷線程(雖然這個(gè)可能性很小,但是只要一旦發(fā)生這種情況就會(huì)造成死循環(huán)了)。
下面解釋一下這段代碼為何有可能導(dǎo)致無法中斷線程。在前面已經(jīng)解釋過,每個(gè)線程在運(yùn)行過程中都有自己的工作內(nèi)存,那么線程1在運(yùn)行的時(shí)候,會(huì)將flag變量的值拷貝一份放在自己的工作內(nèi)存當(dāng)中。
那么當(dāng)線程2更改了flag變量的值之后,但是還沒來得及寫入主存當(dāng)中,線程2轉(zhuǎn)去做其他事情了,那么線程1由于不知道線程2對(duì)flag變量的更改,因此還會(huì)一直循環(huán)下去。
但是用volatile修飾之后就變得不一樣了:
第一:使用volatile關(guān)鍵字會(huì)強(qiáng)制將修改的值立即寫入主存;
第二:使用volatile關(guān)鍵字的話,當(dāng)線程2進(jìn)行修改時(shí),會(huì)導(dǎo)致線程1的工作內(nèi)存中緩存變量flag的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對(duì)應(yīng)的緩存行無效);
第三:由于線程1的工作內(nèi)存中緩存變量flag的緩存行無效,所以線程1再次讀取變量flag的值時(shí)會(huì)去主存讀取。
那么在線程2修改flag值時(shí)(當(dāng)然這里包括2個(gè)操作,修改線程2工作內(nèi)存中的值,然后將修改后的值寫入內(nèi)存),會(huì)使得線程1的工作內(nèi)存中緩存變量flag的緩存行無效,然后線程1讀取時(shí),發(fā)現(xiàn)自己的緩存行無效,它會(huì)等待緩存行對(duì)應(yīng)的主存地址被更新之后,然后去對(duì)應(yīng)的主存讀取最新的值。
那么線程1讀取到的就是最新的正確的值。
volatile boolean inited = false;
//線程1:
context = loadContext();
inited = true;
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
解釋參考5.3的例子2
6.2.單例模式中的double check
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
為什么要使用volatile 修飾instance?
主要在于instance = new Singleton()這句,這并非是一個(gè)原子操作,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情:
1.給 instance 分配內(nèi)存
2.調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
3.將instance對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了)。
但是在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時(shí) instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會(huì)直接返回 instance,然后使用,然后順理成章地報(bào)錯(cuò)。
7.CPU緩存的相關(guān)知識(shí)
7.1.CPU緩存
CPU緩存的出現(xiàn)主要是為了解決CPU運(yùn)算速度與內(nèi)存讀寫速度不匹配的矛盾,因?yàn)镃PU運(yùn)算速度要比內(nèi)存讀寫速度快得多,舉個(gè)例子:
- 一次主內(nèi)存的訪問通常在幾十到幾百個(gè)時(shí)鐘周期
- 一次L1高速緩存的讀寫只需要1~2個(gè)時(shí)鐘周期
- 一次L2高速緩存的讀寫也只需要數(shù)十個(gè)時(shí)鐘周期
這種訪問速度的顯著差異,導(dǎo)致CPU可能會(huì)花費(fèi)很長時(shí)間等待數(shù)據(jù)到來或把數(shù)據(jù)寫入內(nèi)存。
基于此,現(xiàn)在CPU大多數(shù)情況下讀寫都不會(huì)直接訪問內(nèi)存(CPU都沒有連接到內(nèi)存的管腳),取而代之的是CPU緩存,CPU緩存是位于CPU與內(nèi)存之間的臨時(shí)存儲(chǔ)器,它的容量比內(nèi)存小得多但是交換速度卻比內(nèi)存快得多。而緩存中的數(shù)據(jù)是內(nèi)存中的一小部分?jǐn)?shù)據(jù),但這一小部分是短時(shí)間內(nèi)CPU即將訪問的,當(dāng)CPU調(diào)用大量數(shù)據(jù)時(shí),就可先從緩存中讀取,從而加快讀取速度。
按照讀取順序與CPU結(jié)合的緊密程度,CPU緩存可分為:
- 一級(jí)緩存:簡稱L1 Cache,位于CPU內(nèi)核的旁邊,是與CPU結(jié)合最為緊密的CPU緩存。
- 二級(jí)緩存:簡稱L2 Cache,分內(nèi)部和外部兩種芯片,內(nèi)部芯片二級(jí)緩存運(yùn)行速度與主頻相同,外部芯片二級(jí)緩存運(yùn)行速度則只有主頻的一半。
- 三級(jí)緩存:簡稱L3 Cache,部分高端CPU才有。
每一級(jí)緩存中所存儲(chǔ)的數(shù)據(jù)全部都是下一級(jí)緩存中的一部分,這三種緩存的技術(shù)難度和制造成本是相對(duì)遞減的,所以其容量也相對(duì)遞增。
當(dāng)CPU要讀取一個(gè)數(shù)據(jù)時(shí),首先從一級(jí)緩存中查找,如果沒有再從二級(jí)緩存中查找,如果還是沒有再從三級(jí)緩存中或內(nèi)存中查找。一般來說每級(jí)緩存的命中率大概都有80%左右,也就是說全部數(shù)據(jù)量的80%都可以在一級(jí)緩存中找到,只剩下20%的總數(shù)據(jù)量才需要從二級(jí)緩存、三級(jí)緩存或內(nèi)存中讀取。
7.2.使用CPU緩存帶來的問題
用一張圖表示一下 CPU –> CPU緩存 –> 主內(nèi)存 數(shù)據(jù)讀取之間的關(guān)系:

當(dāng)系統(tǒng)運(yùn)行時(shí),CPU執(zhí)行計(jì)算的過程如下:
- 程序以及數(shù)據(jù)被加載到主內(nèi)存
- 指令和數(shù)據(jù)被加載到CPU緩存
- CPU執(zhí)行指令,把結(jié)果寫到高速緩存
- 高速緩存中的數(shù)據(jù)寫回主內(nèi)存
如果服務(wù)器是單核CPU,那么這些步驟不會(huì)有任何的問題,但是如果服務(wù)器是多核CPU,那么問題來了,以Intel Core i7處理器的高速緩存概念模型為例(圖片來自《深入理解計(jì)算機(jī)系統(tǒng)》):

試想下面一種情況:
- 核0讀取了一個(gè)字節(jié),根據(jù)局部性原理,它相鄰的字節(jié)同樣被被讀入核0的緩存
- 核3做了上面同樣的工作,這樣核0與核3的緩存擁有同樣的數(shù)據(jù)
- 核0修改了那個(gè)字節(jié),被修改后,那個(gè)字節(jié)被寫回核0的緩存,但是該信息并沒有寫回主存
- 核3訪問該字節(jié),由于核0并未將數(shù)據(jù)寫回主存,數(shù)據(jù)不同步
為了解決這一問題,CPU制造商規(guī)定了一個(gè)緩存一致性協(xié)議。
7.3.緩存一致性協(xié)議
每個(gè)CPU都有一級(jí)緩存,但是,我們卻無法保證每個(gè)CPU的一級(jí)緩存數(shù)據(jù)都是一樣的。 所以同一個(gè)程序,CPU進(jìn)行切換的時(shí)候,切換前和切換后的數(shù)據(jù)可能會(huì)有不一致的情況。那么這個(gè)就是一個(gè)很大的問題了。 如何保證各個(gè)CPU緩存中的數(shù)據(jù)是一致的。就是CPU的緩存一致性問題。
7.4.總線鎖
一種處理一致性問題的辦法是使用Bus Locking(總線鎖)。當(dāng)一個(gè)CPU對(duì)其緩存中的數(shù)據(jù)進(jìn)行操作的時(shí)候,往總線中發(fā)送一個(gè)Lock信號(hào)。 這個(gè)時(shí)候,所有CPU收到這個(gè)信號(hào)之后就不操作自己緩存中的對(duì)應(yīng)數(shù)據(jù)了,當(dāng)操作結(jié)束,釋放鎖以后,所有的CPU就去內(nèi)存中獲取最新數(shù)據(jù)更新。
但是用鎖的方式總是避不開性能問題??偩€鎖總是會(huì)導(dǎo)致CPU的性能下降。所以出現(xiàn)另外一種維護(hù)CPU緩存一致性的方式,MESI。
7.5.MESI
MESI是保持一致性的協(xié)議。它的方法是在CPU緩存中保存一個(gè)標(biāo)記位,這個(gè)標(biāo)記位有四種狀態(tài):
- M: Modify,修改緩存,當(dāng)前CPU的緩存已經(jīng)被修改了,即與內(nèi)存中數(shù)據(jù)已經(jīng)不一致了;
- E: Exclusive,獨(dú)占緩存,當(dāng)前CPU的緩存和內(nèi)存中數(shù)據(jù)保持一致,而且其他處理器并沒有可使用的緩存數(shù)據(jù);
- S: Share,共享緩存,和內(nèi)存保持一致的一份拷貝,多組緩存可以同時(shí)擁有針對(duì)同一內(nèi)存地址的共享緩存段;
- I: Invalid,失效緩存,這個(gè)說明CPU中的緩存已經(jīng)不能使用了。
CPU的讀取遵循下面幾點(diǎn):
- 如果緩存狀態(tài)是I,那么就從內(nèi)存中讀取,否則就從緩存中直接讀取。
- 如果緩存處于M或E的CPU讀取到其他CPU有讀操作,就把自己的緩存寫入到內(nèi)存中,并將自己的狀態(tài)設(shè)置為S。
- 只有緩存狀態(tài)是M或E的時(shí)候,CPU才可以修改緩存中的數(shù)據(jù),修改后,緩存狀態(tài)變?yōu)镸。
這樣,每個(gè)CPU都遵循上面的方式則CPU的效率就提高上來了。
8.學(xué)習(xí)成果檢查
面試官:Java并發(fā)這塊了解的怎么樣?說說你對(duì)volatile關(guān)鍵字的理解。
面試官:能不能詳細(xì)說下什么是內(nèi)存可見性,什么又是指令重排呢?
面試官:那你具體說說并發(fā)編程的三個(gè)特性呢?
面試官:volatile關(guān)鍵字如何滿足并發(fā)編程的三大特性的?
面試官:volatile的兩點(diǎn)內(nèi)存語義能保證可見性和有序性,但是能保證原子性嗎?
面試官:那你知道volatile底層的實(shí)現(xiàn)機(jī)制?
面試官:你在哪里會(huì)使用到volatile,舉兩個(gè)例子呢?
面試官:單例模式的幾種實(shí)現(xiàn)方式?能手寫一下代碼嗎?
9.Java 中 static 和 volatile 關(guān)鍵字的區(qū)別
static指的是類的靜態(tài)成員,實(shí)例間共享
volatile跟Java的內(nèi)存模型有關(guān),線程執(zhí)行時(shí)會(huì)將變量從主內(nèi)存加載到線程工作內(nèi)存,建立一個(gè)副本,在某個(gè)時(shí)刻寫回。valatile指的每次都讀取主內(nèi)存的值,有更新則立即寫回主內(nèi)存。
理解了這兩點(diǎn),逐句再來解釋你的困惑:
“既然static保證了唯一性”:static保證唯一性,指的是static修飾的靜態(tài)成員變量是唯一的,多個(gè)實(shí)例共享這唯一一個(gè)成員。
“那么他對(duì)多個(gè)線程來說都是可見的啊”:這里,static其實(shí)跟線程沒太大關(guān)系,應(yīng)該說對(duì)多個(gè)對(duì)象實(shí)例是可見的。你說對(duì)多個(gè)線程可見,雖然沒什么毛病,因?yàn)殪o態(tài)變量全局可見嘛,但是把這個(gè)理解轉(zhuǎn)到線程的上線文中是困惑的起因。
“volatile保證了線程之間的可見性”:因?yàn)榫€程看到volatile變量會(huì)去讀取主內(nèi)存最新的值,而不是自個(gè)一直在那跟內(nèi)部的變量副本玩,所以保證了valatile變量在各個(gè)線程間的可見性。
“那么修改的時(shí)候只要是原子操作,那么就會(huì)保證它的唯一性了吧”:此時(shí)你說的“唯一性”,指的是各個(gè)線程都能讀取到唯一的最新的主內(nèi)存變量,消除了線程工作內(nèi)存加載變量副本可能帶來的線程之間的“不唯一性”。這里“唯一性”的含義跟第一句說的“唯一性”是不一樣的。
“這兩個(gè)在我理解上我覺得差不多?!保浩鋵?shí)解決問題的“場景”是完全不一樣的。
造成理解困惑最大的原因在于,這兩個(gè)場景略有類似,以致混淆了:
場景1:各個(gè)類的實(shí)例共享唯一一個(gè)類靜態(tài)變量
場景2:各個(gè)線程共同讀取唯一的最新的主內(nèi)存變量的值,只保證可見性,它不足以保證數(shù)據(jù)的同步性。
參考http://blog.sina.com.cn/s/blog_4e1e357d0101i486.html
- volatile是告訴編譯器,每次取這個(gè)變量的值都需要從主存中取,而不是用自己線程工作內(nèi)存中的緩存.
- static 是說這個(gè)變量,在主存中所有此類的實(shí)例用的是同一份,各個(gè)線程創(chuàng)建時(shí)需要從主存同一個(gè)位置拷貝到自己工作內(nèi)存中去(而不是拷貝此類不同實(shí)例中的這個(gè)變量的值),也就是說只能保證線程創(chuàng)建時(shí),變量的值是相同來源的,運(yùn)行時(shí)還是使用各自工作內(nèi)存中的值,依然會(huì)有不同步的問題.
10.擴(kuò)展閱讀
Java多線程——線程范圍內(nèi)共享變量和ThreadLocal
Java多線程共享變量控制
感謝網(wǎng)友的分享:
http://www.importnew.com/24082.html
http://www.cnblogs.com/dolphin0520/p/3920373.html
https://blog.csdn.net/wanghai__/article/details/6260178
http://www.techug.com/post/java-volatile-keyword.html