JUC系列整體欄目
內(nèi)容 | 鏈接地址 |
---|---|
【一】深入理解JMM內(nèi)存模型的底層實(shí)現(xiàn)原理 | https://zhenghuisheng.blog.csdn.net/article/details/132400429 |
【二】深入理解CAS底層原理和基本使用 | https://blog.csdn.net/zhenghuishengq/article/details/132478786 |
【三】熟練掌握Atomic原子系列基本使用 | https://blog.csdn.net/zhenghuishengq/article/details/132543379 |
【四】精通Synchronized底層的實(shí)現(xiàn)原理 | https://blog.csdn.net/zhenghuishengq/article/details/132740980 |
【五】通過(guò)源碼分析AQS和ReentrantLock的底層原理 | https://blog.csdn.net/zhenghuishengq/article/details/132857564 |
【六】深入理解Semaphore底層原理和基本使用 | https://blog.csdn.net/zhenghuishengq/article/details/132908068 |
【七】深入理解CountDownLatch底層原理和基本使用 | https://blog.csdn.net/zhenghuishengq/article/details/133343440 |
【八】深入理解CyclicBarrier底層原理和基本使用 | https://blog.csdn.net/zhenghuishengq/article/details/133378623 |
一,深入理解JMM內(nèi)存模型
1,什么是可見性
在談jmm的內(nèi)存模型之前,先了解一下并發(fā)并發(fā)編程的三大特性,分別是:可見性,原子性,有序性。可見性指的就是當(dāng)一個(gè)線程修改某個(gè)變量的值之后,其他的線程可以立馬感知到。
接下來(lái)看一個(gè)例子,看一個(gè)線程改變值之后,另一個(gè)線程能否立馬感知到這個(gè)值被改變了。
public class JmmTest {
private boolean flag = true;
private int count = 0;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
}
public void load() {
while (flag) {
//TODO 業(yè)務(wù)邏輯
count++;
}
System.out.println(Thread.currentThread().getName() + "跳出循環(huán): count=" + count);
}
public static void main(String[] args) throws InterruptedException {
JmmTest test = new JmmTest();
// 線程threadA模擬數(shù)據(jù)加載場(chǎng)景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 讓threadA執(zhí)行一會(huì)兒
Thread.sleep(1000);
// 線程threadB通過(guò)flag控制threadA的執(zhí)行時(shí)間
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
}
可以發(fā)現(xiàn)以上操作,線程A先加載這個(gè)flag值,由于是true,因此一直處于while循環(huán)中空轉(zhuǎn),但是線程B隨后修改了這個(gè)值,但是可以發(fā)現(xiàn)線程A是還在這個(gè)while循環(huán)中的,并沒(méi)有跳出循環(huán),其結(jié)果值如下:
threadB修改flag:false
也就是說(shuō),在一個(gè)正常的多線程之間的通信,是不能夠直接的進(jìn)行通信的,因此這就需要了解JMM的底層原理了
2,什么是JMM
Java Memory Model ,就是JMM的全稱,意思是java內(nèi)存模型。主要用于規(guī)范java虛擬機(jī)和計(jì)算機(jī)內(nèi)存時(shí)如何協(xié)調(diào)工作的,規(guī)定了當(dāng)一個(gè)線程改變某個(gè)共享變量值后,其他線程需要如何查看以及合適可以查看這個(gè)被改變的共享數(shù)據(jù)。
jmm的內(nèi)存模型如下,java采用的是共享變量的模型方式,在創(chuàng)建一個(gè)共享變量之后,這些共享變量時(shí)存儲(chǔ)在主內(nèi)存中的,所有線程都能訪問(wèn),但是每個(gè)線程需要操作這個(gè)變量時(shí),需要先將這個(gè)值加載到每個(gè)線程的工作內(nèi)存中,即每個(gè)線程都有對(duì)應(yīng)棧幀,將這個(gè)值加入到局部變量表即可,就成為了共享變量的一個(gè)副本,隨后線程A才能去修改這個(gè)值
而由于主內(nèi)存中的變量都是共享變量,因此為了解決并發(fā)問(wèn)題,在JMM內(nèi)部又引入了八大原子操作
1,lock:作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)記為一條線程獨(dú)占狀態(tài)
2,unlock:把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定
3,read(讀取):作用于主內(nèi)存中,需要先對(duì)變量進(jìn)行副本的拷貝,然后將變量值傳輸?shù)焦ぷ鲀?nèi)存中
4,load(載入):在工作內(nèi)存中,需要對(duì)傳輸過(guò)來(lái)的副本變量進(jìn)行一個(gè)獲取,并且存入到工作內(nèi)存中
5,use(使用): 需要將獲取的變量傳給執(zhí)行引擎
6,assign(賦值):執(zhí)行引擎會(huì)將這個(gè)收到的變量賦值給工作內(nèi)存的變量
7,store(存儲(chǔ)):修改這個(gè)傳過(guò)來(lái)的副本之后,會(huì)將修改的值存儲(chǔ)并送到主內(nèi)存中
8,write(寫入):會(huì)將這個(gè)存儲(chǔ)的變量寫回到主內(nèi)存中,即修改主內(nèi)存的值
如當(dāng)一個(gè)線程去修改主內(nèi)存中的共享變量的方式如下,比如說(shuō)內(nèi)存中的 x = 5 進(jìn)行 +1 的操作如下圖所示,首先線程A會(huì)read讀取主內(nèi)存中的x = 5的值,隨后將讀取到的值load載入到線程A的本地內(nèi)存中,一般棧幀中存放變量的都是這個(gè)局部變量表,隨后會(huì)通過(guò)use的指令使用這個(gè)變量,將這個(gè)值加入到cpu中,結(jié)果cpu內(nèi)部的運(yùn)算之后,此時(shí) x = 6,會(huì)通過(guò)assign方式將這個(gè)結(jié)果值從cpu返回到本地內(nèi)存中,隨后將這個(gè)值返回到主內(nèi)存中,并通過(guò)store的方式將這個(gè)值存儲(chǔ),最后將被修改的變量寫回到主內(nèi)存中。
同時(shí)在使用這八種原子操作時(shí),需要滿足以下的規(guī)則
- 如果要把一個(gè)變量從主內(nèi)存中復(fù)制到工作內(nèi)存,就需要按順尋地執(zhí)行read和load操作, 如果把變量從工作內(nèi)存中同步回主內(nèi)存中,就要按順序地執(zhí)行store和write操作。但Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒(méi)有保證必須是連續(xù)執(zhí)行。
- 不允許read和load、store和write操作之一單獨(dú)出現(xiàn)
- 不允許一個(gè)線程丟棄它的最近assign的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中。
- 不允許一個(gè)線程無(wú)原因地(沒(méi)有發(fā)生過(guò)任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中。
- 一個(gè)新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量。即就是對(duì)一個(gè)變量實(shí)施use和store操作之前,必須先執(zhí)行過(guò)了assign和load操作。
- 一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖。lock和unlock必須成對(duì)出現(xiàn)
- 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
- 如果一個(gè)變量事先沒(méi)有被lock操作鎖定,則不允許對(duì)它執(zhí)行unlock操作;也不允許去unlock一個(gè)被其他線程鎖定的變量。
- 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)。
3,引入volatile
在了解完這個(gè)jmm內(nèi)存模型之后,知道java線程之間是如何進(jìn)行線程通信的,再回到這個(gè) JmmTest 方法中,現(xiàn)在可以大膽的猜測(cè)一下,是不是因?yàn)榫€程B修改完值后,沒(méi)有人去通知線程A?所以才導(dǎo)致值沒(méi)有發(fā)生變化
因此接下來(lái)繼續(xù)驗(yàn)證,就是直接在這個(gè)flag變量前面增加一個(gè)關(guān)鍵字 volatile
private volatile boolean flag = true;
其結(jié)果如下,可以得出結(jié)論,線程A跳出了循環(huán),就是意味著線程A接收到了這個(gè)最新的值
threadB修改flag:false
threadA跳出循環(huán): count=399766740
因此查閱了一些資料,以及看了一下hotspot里面關(guān)于這個(gè)volatile關(guān)鍵字的源碼,可以發(fā)現(xiàn)這個(gè)關(guān)鍵字是通過(guò)一個(gè)JVM的內(nèi)存屏障來(lái)實(shí)現(xiàn)的。
storeload(); //jvm內(nèi)存屏障,在匯編指令中,對(duì)應(yīng)著lock關(guān)鍵字
內(nèi)存屏障可以禁止該指令與前面和后面的讀寫指令重排序,并且可以使其他線程中的本地內(nèi)存中的該值直接失效,這樣其他內(nèi)存就需要去主內(nèi)存中獲取改值,就能拿到最新的值了。因此volatile是通過(guò)內(nèi)存屏障的方式來(lái)實(shí)現(xiàn)數(shù)據(jù)的可見性和有序性的。
除了這個(gè)volatile關(guān)鍵字之外,另外像synchronized,lock等這些鎖底層都是采用了這個(gè)內(nèi)存屏障來(lái)實(shí)現(xiàn),因此這些重量級(jí)鎖肯定也是可以保證可見性和有序性的,同時(shí)由于是重量級(jí)操作,除了這兩種之外,他們同時(shí)還能保證原子性。
除了內(nèi)存屏障可以保證可見性之外,關(guān)鍵字final也是可以保證可見性的??偠灾鼙WC可見性的方式只有兩種:一種是內(nèi)存屏障,一種是上下文切換
4,cpu緩存架構(gòu)
在cpu中,主要由寄存器,程序計(jì)數(shù)器,高速緩存,邏輯運(yùn)算單元組成,高速緩存又分了三級(jí)緩存,分別是一級(jí)緩存、二級(jí)緩存和三級(jí)緩存,一級(jí)緩存中又分為兩部分,一個(gè)用于存儲(chǔ)指令,一個(gè)用于存儲(chǔ)數(shù)據(jù)。在inter處理器中,一個(gè)cpu又分為兩個(gè)處理器,因此會(huì)存在兩個(gè)cpu共享一個(gè)三級(jí)緩存的情況。
使用高速緩存主要是減少等待內(nèi)存的時(shí)間,提升CPU的計(jì)算能力
接下來(lái)根據(jù)這個(gè)緩存架構(gòu)再舉一個(gè)例子,現(xiàn)在有兩個(gè)線程,分別是線程thread1和線程thread2,假設(shè)主內(nèi)存中有一個(gè)值x=100,接下來(lái)兩個(gè)線程同時(shí)去讀這個(gè)100,線程1加對(duì)這個(gè)值加10,線程2對(duì)這個(gè)值加20,那么根據(jù)JMM的八大原子操作,此時(shí)線程1的CPU的值為110,線程2的CPU的值為120,最終會(huì)將這個(gè)值寫回主內(nèi)存中。
那么此時(shí)主內(nèi)存就會(huì)出現(xiàn)兩種情況,如果線程1先寫回,線程2后寫回,那么線程2會(huì)將線程1寫回的值覆蓋掉,此時(shí);如果線程2先寫回,線程后寫回,那么線程會(huì)將線程2寫回的值給覆蓋掉,這就是經(jīng)典的線程不安全問(wèn)題
造成這種原因的主要問(wèn)題,是因?yàn)榫彺娌灰恢碌膯?wèn)題。 即線程1的高速緩存的值和線程2的高速緩存的值不一致所導(dǎo)致的,因此為了解決這種緩存一致性的問(wèn)題,主要有兩種解決方式:嗅探機(jī)制、基于目錄的機(jī)制
5,嗅探機(jī)制
再了解完這個(gè)導(dǎo)致數(shù)據(jù)不安全的原因是由于緩存不一致的問(wèn)題,因此為了解決這個(gè)硬件層面的緩存一致性,最流行的還是使用這種嗅探機(jī)制。
其工作原理如下:就是說(shuō)如果存在多個(gè)緩存被共享的時(shí)候,如果有處理器修改了共享變量的值,那么必須傳播到其他所有具有該變量的副本中,通過(guò)這種傳播機(jī)制來(lái)防止系統(tǒng)違反緩存的一致性。就是說(shuō),數(shù)據(jù)的變更通知是通過(guò)總線來(lái)完成的。當(dāng)其他緩存接收到這個(gè)通知信息之后,可以選擇重新的在主內(nèi)存中刷新數(shù)據(jù),也可以直接讓當(dāng)前緩存中的值直接失效,具體是哪種做法,還得取決于使用哪種緩存一致性協(xié)議。
寫失效:就是某個(gè)處理器將值改完之后,直接通知其他處理器,讓其他處理器的緩存值失效
寫更新:就是處理器將值修改完之后,在通知其他處理器的時(shí)候,直接將值攜帶上,讓其他的處理器緩存值更新
總線的帶寬是有效的,因此寫失效的使用范圍是最廣的。MSI、MESI、MOSI、MOESI等是最常見的緩存一致性協(xié)議
6,解決緩存一致性的MESI
為了解決緩存一致性,使用最多的方式是這種MESI的方式,總共有四種狀態(tài),分別是
- M:modify,修改狀態(tài)
- E:Exclusive,獨(dú)占狀態(tài)
- S:Share,共享狀態(tài)
- I:Invalid,失效狀態(tài)
當(dāng)工作內(nèi)存將主內(nèi)存的值加載到高速緩存之后,假設(shè)此時(shí)只有當(dāng)前線程thread1加載了X=5,那么此時(shí)X是一個(gè)Exclusive獨(dú)占狀態(tài),如果此時(shí)線程thread2也加載了這個(gè)值,那么此時(shí)該值則會(huì)從一個(gè)獨(dú)占狀態(tài)變成一個(gè)Share共享狀態(tài),如果此時(shí)線程thread1要修改這個(gè)值,那么在修改這個(gè)值后,X就會(huì)從一個(gè)共享狀態(tài)變?yōu)橐粋€(gè)Modify修改狀態(tài),并且在回顯的時(shí)候被總線窺探到,總線就會(huì)發(fā)起請(qǐng)求告訴其他的線程這個(gè)被修改的值,讓其他的線程緩存里面的改值直接失效Invalid,那么其他線程就可以去獲取最新的值。
但是該協(xié)議并不是會(huì)直接生效,而是需要在特定的時(shí)候生效,就是需要一個(gè)lock前綴指令才可以滿足該協(xié)議,如一些常見的volatile,synchronized,lock等關(guān)鍵字。這樣才能解決這種緩存一致性的問(wèn)題。但是volatile并不能保證原子性。
并且在某個(gè)線程更新了某個(gè)值之后,刷新主內(nèi)存的線程會(huì)立即執(zhí)行,這樣才能讓其他已經(jīng)處于失效的線程立馬的回到主內(nèi)存中去更新改值,從而線程在獲取值時(shí)減少數(shù)據(jù)的臟讀問(wèn)題以及長(zhǎng)時(shí)間等待的問(wèn)題。 通過(guò)緩存一致性,來(lái)保證在多線程的情況下實(shí)現(xiàn)共享變量的可見性
除了緩存一致性協(xié)議之外,還有總線一致性協(xié)議,由于總線一致性的性能問(wèn)題,緩存一致性協(xié)議才得以出現(xiàn)。
7,JMM內(nèi)存可見性的保證
在單線程中:由于需要保證 else-if-serial 規(guī)范,即不管如何進(jìn)行指令重排,都必須要保證最終結(jié)果的一致性,因此,單線程不存在內(nèi)存可見性的問(wèn)題,不管是編譯器還是及時(shí)處理器等,都必須保證和原始順序所執(zhí)行的結(jié)果值相同
在正確同步的多線程中:如在加鎖的情況下,JMM在內(nèi)部會(huì)禁止指令重排的操作,并且在底層會(huì)通過(guò)內(nèi)存屏障的操作來(lái)操作底層硬件,從而實(shí)現(xiàn)可見性和有序性的操作。
未同步的多線程:JMM不能保證未同步的執(zhí)行結(jié)果與順序一致性的結(jié)果一致。由于在JVM中,存在一些JIT即時(shí)編譯器以及解釋器的一些優(yōu)化等,因此就會(huì)出現(xiàn)指令重排的情況。
x = 10; y = 100;
y = 100; ====> x = 10;
z = x + 10; z = x + 10;
舉個(gè)例子,如在單例模式加鎖的雙重檢測(cè)中,需要在對(duì)象的前面加一個(gè)關(guān)鍵字 volatile,如果不加的話,在new對(duì)象的時(shí)候,會(huì)經(jīng)歷以下步驟:開辟內(nèi)存空間,堆內(nèi)存初始化,棧中對(duì)象指向堆中對(duì)象。這里就會(huì)出現(xiàn)一個(gè)問(wèn)題,由于new對(duì)象并沒(méi)有保證這個(gè)原子操作,因此就會(huì)出現(xiàn)指令重排的情況,就是可能會(huì)先指向堆中的對(duì)象,再在堆內(nèi)存中初始化,就是第二步和第三步的順序可能會(huì)發(fā)生改變。
public class SingletonTest{
private volatile static SingletonTest instance = null;
private SingletonTest() {}
public static SingletonTest getInstance() {
if (instance == null) {
synchronized (SingletonTest.class) {
if (instance == null) {
//在不加volatile或者其他鎖的情況下
//可能會(huì)出現(xiàn)指令重排的情況
instance = new Singleton();
}
}
}
return instance;
}
}
那在多線程的情況下,在第一個(gè)線程正好執(zhí)行到發(fā)生指令重排的第二步,就是指向了一個(gè)堆中的對(duì)象,但還沒(méi)有初始化,只是經(jīng)歷了實(shí)例化,而第二個(gè)線程進(jìn)行第一個(gè)if判斷的時(shí)候,此時(shí)并沒(méi)有加鎖,所以發(fā)現(xiàn)不為null,就直接return了,但是return的是一個(gè)你有進(jìn)行初始化的一個(gè)值,因此返回的對(duì)象肯定是有問(wèn)題的
所以為了解決這個(gè)指令重排的問(wèn)題,就需要在這個(gè)對(duì)象上面加上volatile這個(gè)關(guān)鍵字了,這樣就能禁止指令重排了
private volatile static SingletonTest instance = null;
8,內(nèi)存屏障
在jvm和硬件層面都有實(shí)現(xiàn)內(nèi)存屏障的方式。
在jvm層面,在JSR規(guī)范中定義了四種內(nèi)存屏障,分別是LoadStore,LoadLoad,StoreLoad,StoreStore。Load操作可以當(dāng)做成是一個(gè)read讀取操作,Store操作可以當(dāng)做成是一個(gè)寫入操作,兩個(gè)操作之間相當(dāng)于加了一個(gè)一堵墻,從而保證兩個(gè)操作的順序不被打亂
LoadStore:在store2指令寫入數(shù)據(jù)之前,保證數(shù)據(jù)一定被load1指令先寫入進(jìn)去
LoadLoad:在Load2指令讀取數(shù)據(jù)之前,保證數(shù)據(jù)一定被load1指令先讀取出來(lái)
StoreLoad:在Load2指令讀取數(shù)據(jù)之前,保證數(shù)據(jù)一定被Store指令寫入進(jìn)去
StoreStore:在store2指令寫入數(shù)據(jù)之前,保證數(shù)據(jù)一定被load1指令讀取出來(lái)
并且以上的寫入操作,都是可以實(shí)現(xiàn)所有的處理器都可以感知到數(shù)據(jù)的變化,即保證可見性。當(dāng)前jvm底層實(shí)現(xiàn)內(nèi)存屏障的方式主要是通過(guò)這個(gè)StoreLoad方式來(lái)實(shí)現(xiàn)的。
在硬件層面,也提供了一系列的內(nèi)存屏障的方式保證數(shù)據(jù)的一致性,主要是通過(guò)ifence和sfence來(lái)實(shí)現(xiàn)讀寫屏障,也可以通過(guò)Lock前綴來(lái)實(shí)現(xiàn)這個(gè)類似于內(nèi)存屏障的功能。但是在JMM內(nèi)存模型中屏蔽了這種底層硬件帶來(lái)的差異,直接由JVM來(lái)為不同的平臺(tái)生成相應(yīng)的字節(jié)碼。通過(guò)內(nèi)存屏障的方式,來(lái)保證共享變量的有序性
9,為何多線程的累加值總是小于期待值
了解這個(gè)JMM的內(nèi)存模型之后,接下來(lái)通過(guò)之前的多線程的系列的文章,來(lái)對(duì)上述這個(gè)問(wèn)題做一個(gè)初步的了解。
count++;
由于在java中,實(shí)現(xiàn)線程的方式是使用的內(nèi)核態(tài)的方式實(shí)現(xiàn)的多線程,也就是說(shuō)開發(fā)者只能通過(guò)內(nèi)核去調(diào)用操作系統(tǒng),再去調(diào)用線程,因此開發(fā)人員并不能控制線程,因此就不能控制上下文切換等,并且實(shí)現(xiàn)線程的方式是搶占式的方式實(shí)現(xiàn),所以在累加操作中,某個(gè)值可能只執(zhí)行了一半,就出現(xiàn)了cpu中時(shí)間片的切換,導(dǎo)致這個(gè)值被其他線程操作,如果是在多線程的情況下,兩個(gè)線程同時(shí)操作一個(gè)值,就會(huì)出現(xiàn)這種值被覆蓋的問(wèn)題。因此最終出現(xiàn)的結(jié)果會(huì)小于期待值
其次是通過(guò)JMM模型可知,每個(gè)線程都有屬于自己的工作區(qū)間,但是每個(gè)線程在將值修改之后,其他線程并不能感知到,就是無(wú)法保證可見性的問(wèn)題,因此也會(huì)出現(xiàn)大量的值被覆蓋。所以累加的結(jié)構(gòu)也會(huì)小于期待值文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-661302.html
因此需要通過(guò)加鎖的方式強(qiáng)行保證線程間執(zhí)行順序,以及需要通過(guò)實(shí)現(xiàn)內(nèi)存屏障的方式來(lái)實(shí)現(xiàn)線程間的可見性和有序性以及原子性。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-661302.html
到了這里,關(guān)于【JUC系列-01】深入理解JMM內(nèi)存模型的底層實(shí)現(xiàn)原理的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!