小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

分布式并發(fā)編程,線程安全性,原理分析

 菌心說 2021-09-15

初步認(rèn)識(shí) Volatile

一段代碼引發(fā)的思考

下面這段代碼,演示了一個(gè)使用 volatile 以及沒使用volatile這個(gè)關(guān)鍵字,對(duì)于變量更新的影響

public class VolatileDemo {public /*volatile*/ static boolean stop=false;public static void main(String[] args) throwsInterruptedException {Thread thread=new Thread(()->{int i=0;while(!stop){i++;}});thread.start();System.out.println('begin start thread');Thread.sleep(1000);}}

volatile 的作用

volatile 可以使得在多處理器環(huán)境下保證了共享變量的可見性,那么到底什么是可見性呢?不知道大家有沒有思考過這個(gè)問題

在單線程的環(huán)境下,如果向一個(gè)變量先寫入一個(gè)值,然后在沒有寫干涉的情況下讀取這個(gè)變量的值,那這個(gè)時(shí)候讀取到的這個(gè)變量的值應(yīng)該是之前寫入的那個(gè)值。這本來是一個(gè)很正常的事情。但是在多線程環(huán)境下,讀和寫發(fā)生在不同的線程中的時(shí)候,可能會(huì)出現(xiàn):讀線程不能及時(shí)的讀取到其他線程寫入的最新的值。這就是所謂的可見性

為了實(shí)現(xiàn)跨線程寫入的內(nèi)存可見性,必須使用到一些機(jī)制來實(shí)現(xiàn)。而 volatile 就是這樣一種機(jī)制

volatile 關(guān)鍵字是如何保證可見性的?

我們可以使用【hsdis】這個(gè)工具,來查看前面演示的這段代碼的匯編指令,具體的使用請(qǐng)查看使用說明文檔

在運(yùn)行的代碼中,設(shè)置 jvm參數(shù)如下

【-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*App.*(替換成實(shí)際運(yùn)行的代碼)】

然后在輸出的結(jié)果中,查找下 lock指令,會(huì)發(fā)現(xiàn),在修改帶有 volatile修飾的成員變量時(shí),會(huì)多一個(gè) lock指令。lock是一種控制指令,在多處理器環(huán)境下,lock 匯編指令可以基于總線鎖或者緩存鎖的機(jī)制來達(dá)到可見性的一個(gè)效果。

為了讓大家更好的理解可見性的本質(zhì),我們需要從硬件層面進(jìn)行梳理

從硬件層面了解可見性的本質(zhì)

一臺(tái)計(jì)算機(jī)中最核心的組件是 CPU、內(nèi)存、以及 I/O設(shè)備。在整個(gè)計(jì)算機(jī)的發(fā)展歷程中,除了 CPU、內(nèi)存以及 I/O設(shè)備不斷迭代升級(jí)來提升計(jì)算機(jī)處理性能之外,還有一個(gè)非常核心的矛盾點(diǎn),就是這三者在處理速度的差異。CPU的計(jì)算速度是非常快的,內(nèi)存次之、最后是 IO設(shè)備比如磁盤。而在絕大部分的程序中,一定會(huì)存在內(nèi)存訪問,有些可能還會(huì)存在 I/O設(shè)備的訪問

為了提升計(jì)算性能,CPU 從單核升級(jí)到了多核甚至用到了超線程技術(shù)最大化提高 CPU 的處理性能,但是僅僅提升CPU性能還不夠,如果后面兩者的處理性能沒有跟上,意味著整體的計(jì)算效率取決于最慢的設(shè)備。為了平衡三者的速度差異,最大化的利用 CPU提升性能,從硬件、操作系統(tǒng)、編譯器等方面都做出了很多的優(yōu)化

1. CPU增加了高速緩存

2. 操作系統(tǒng)增加了進(jìn)程、線程。通過 CPU的時(shí)間片切換最大化的提升 CPU 的使用率

3.編譯器的指令優(yōu)化,更合理的去利用好 CPU的高速緩存然后每一種優(yōu)化,都會(huì)帶來相應(yīng)的問題,而這些問題也是導(dǎo)致線程安全性問題的根源。為了了解前面提到的可見性問題的本質(zhì),我們有必要去了解這些優(yōu)化的過程

CPU 高速緩存

線程是 CPU調(diào)度的最小單元,線程設(shè)計(jì)的目的最終仍然是更充分的利用計(jì)算機(jī)處理的效能,但是絕大部分的運(yùn)算任務(wù)不能只依靠處理器“計(jì)算”就能完成,處理器還需要與內(nèi)存交互,比如讀取運(yùn)算數(shù)據(jù)、存儲(chǔ)運(yùn)算結(jié)果,這個(gè) I/O 操作是很難消除的。而由于計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算速度差距非常大,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都會(huì)增加一層讀寫速度盡可能接近處理器運(yùn)算速度的高速緩存來作為內(nèi)存和處理器之間的緩沖:將運(yùn)算需要使用的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱降絻?nèi)存之中。

文章圖片1

通過高速緩存的存儲(chǔ)交互很好的解決了處理器與內(nèi)存的速度矛盾,但是也為計(jì)算機(jī)系統(tǒng)帶來了更高的復(fù)雜度,因?yàn)樗肓艘粋€(gè)新的問題,緩存一致性。

什么叫緩存一致性呢?

首先,有了高速緩存的存在以后,每個(gè) CPU 的處理過程是,先將計(jì)算需要用到的數(shù)據(jù)緩存在 CPU高速緩存中,在 CPU進(jìn)行計(jì)算時(shí),直接從高速緩存中讀取數(shù)據(jù)并且在計(jì)算完成之后寫入到緩存中。在整個(gè)運(yùn)算過程完成后,再把緩存中的數(shù)據(jù)同步到主內(nèi)存。

由于在多CPU種,每個(gè)線程可能會(huì)運(yùn)行在不同的CPU內(nèi),并且每個(gè)線程擁有自己的高速緩存。同一份數(shù)據(jù)可能會(huì)被緩存到多個(gè) CPU 中,如果在不同 CPU 中運(yùn)行的不同線程看到同一份內(nèi)存的緩存值不一樣就會(huì)存在緩存不一致的問題

為了解決緩存不一致的問題,在 CPU 層面做了很多事情,主要提供了兩種解決辦法

1. 總線鎖

2. 緩存鎖

總線鎖和緩存鎖

總線鎖,簡(jiǎn)單來說就是,在多 cpu 下,當(dāng)其中一個(gè)處理器要對(duì)共享內(nèi)存進(jìn)行操作的時(shí)候,在總線上發(fā)出一個(gè) LOCK#信號(hào),這個(gè)信號(hào)使得其他處理器無法通過總線來訪問到共享內(nèi)存中的數(shù)據(jù),總線鎖定把 CPU 和內(nèi)存之間的通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內(nèi)存地址的數(shù)據(jù),所以總線鎖定的開銷比較大,這種機(jī)制顯然是不合適的如何優(yōu)化呢?最好的方法就是控制鎖的保護(hù)粒度,我們只需要保證對(duì)于被多個(gè) CPU 緩存的同一份數(shù)據(jù)是一致的就行。所以引入了緩存鎖,它核心機(jī)制是基于緩存一致性協(xié)議來實(shí)現(xiàn)的。

緩存一致性協(xié)議

為了達(dá)到數(shù)據(jù)訪問的一致,需要各個(gè)處理器在訪問緩存時(shí)遵循一些協(xié)議,在讀寫時(shí)根據(jù)協(xié)議來操作,常見的協(xié)議有MSI,MESI,MOSI等。最常見的就是MESI協(xié)議。接下來給大家簡(jiǎn)單講解一下MESI

MESI表示緩存行的四種狀態(tài),分別是

1. M(Modify) 表示共享數(shù)據(jù)只緩存在當(dāng)前 CPU 緩存中,并且是被修改狀態(tài),也就是緩存的數(shù)據(jù)和主內(nèi)存中的數(shù)據(jù)不一致

2. E(Exclusive) 表示緩存的獨(dú)占狀態(tài),數(shù)據(jù)只緩存在當(dāng)前CPU緩存中,并且沒有被修改

3. S(Shared) 表示數(shù)據(jù)可能被多個(gè) CPU緩存,并且各個(gè)緩存中的數(shù)據(jù)和主內(nèi)存數(shù)據(jù)一致

4. I(Invalid) 表示緩存已經(jīng)失效

在 MESI 協(xié)議中,每個(gè)緩存的緩存控制器不僅知道自己的讀寫操作,而且也監(jiān)聽(snoop)其它 Cache的讀寫操作

文章圖片2
文章圖片3
文章圖片4

對(duì)于 MESI 協(xié)議,從 CPU 讀寫角度來說會(huì)遵循以下原則:CPU讀請(qǐng)求:緩存處于M、E、S狀態(tài)都可以被讀取,I狀態(tài)CPU只能從主存中讀取數(shù)據(jù)

CPU寫請(qǐng)求:緩存處于M、E 狀態(tài)才可以被寫。對(duì)于 S狀態(tài)的寫,需要將其他 CPU中緩存行置為無效才可寫

使用總線鎖和緩存鎖機(jī)制之后,CPU對(duì)于內(nèi)存的操作大概可以抽象成下面這樣的結(jié)構(gòu)。從而達(dá)到緩存一致性效果

文章圖片5

總結(jié)可見性的本質(zhì)

由于 CPU 高速緩存的出現(xiàn)使得 如果多個(gè) cpu 同時(shí)緩存了相同的共享數(shù)據(jù)時(shí),可能存在可見性問題。也就是 CPU0修改了自己本地緩存的值對(duì)于 CPU1 不可見。不可見導(dǎo)致的后果是 CPU1 后續(xù)在對(duì)該數(shù)據(jù)進(jìn)行寫入操作時(shí),是使用的臟數(shù)據(jù)。使得數(shù)據(jù)最終的結(jié)果不可預(yù)測(cè)。

很多同學(xué)肯定希望想在代碼里面去模擬一下可見性的問題,實(shí)際上,這種情況很難模擬。因?yàn)槲覀儫o法讓某個(gè)線程指定某個(gè)特定 CPU,這是系統(tǒng)底層的算法, JVM 應(yīng)該也是沒法控制的。還有最重要的一點(diǎn),就是你無法預(yù)測(cè) CPU緩存什么時(shí)候會(huì)把值傳給主存,可能這個(gè)時(shí)間間隔非常短,短到你無法觀察到。最后就是線程的執(zhí)行的順序問題,因?yàn)槎嗑€程你無法控制哪個(gè)線程的某句代碼會(huì)在另一個(gè)線程的某句代碼后面馬上執(zhí)行。

所以我們只能基于它的原理去了解這樣一個(gè)存在的客觀事實(shí)了解到這里,大家應(yīng)該會(huì)有一個(gè)疑問,剛剛不是說基于緩存一致性協(xié)議或者總線鎖能夠達(dá)到緩存一致性的要求嗎?為什么還需要加volatile 關(guān)鍵字?或者說為什么還會(huì)存在可見性問題呢?

MESI 優(yōu)化帶來的可見性問題

MESI協(xié)議雖然可以實(shí)現(xiàn)緩存的一致性,但是也會(huì)存在一些問題。

就是各個(gè) CPU緩存行的狀態(tài)是通過消息傳遞來進(jìn)行的。如果CPU0 要對(duì)一個(gè)在緩存中共享的變量進(jìn)行寫入,首先需要發(fā)送一個(gè)失效的消息給到其他緩存了該數(shù)據(jù)的 CPU。并且要等到他們的確認(rèn)回執(zhí)。CPU0 在這段時(shí)間內(nèi)都會(huì)處于阻塞狀態(tài)。為了避免阻塞帶來的資源浪費(fèi)。在 cpu 中引入了Store Bufferes。

文章圖片6

CPU0 只需要在寫入共享數(shù)據(jù)時(shí),直接把數(shù)據(jù)寫入到 store bufferes中,同時(shí)發(fā)送 invalidate消息,然后繼續(xù)去處理其他指令。

當(dāng)收到其他所有CPU發(fā)送了 invalidate acknowledge消息時(shí),再將 store bufferes 中的數(shù)據(jù)數(shù)據(jù)存儲(chǔ)至 cache line中。最后再?gòu)木彺嫘型降街鲀?nèi)存。

文章圖片7

但是這種優(yōu)化存在兩個(gè)問題

1. 數(shù)據(jù)什么時(shí)候提交是不確定的,因?yàn)樾枰却渌?cpu給回復(fù)才會(huì)進(jìn)行數(shù)據(jù)同步。這里其實(shí)是一個(gè)異步操作2. 引入了 storebufferes后,處理器會(huì)先嘗試從 storebuffer中讀取值,如果 storebuffer 中有數(shù)據(jù),則直接從storebuffer中讀取,否則就再?gòu)木彺嫘兄凶x取

我們來看一個(gè)例子

文章圖片8

exeToCPU0和exeToCPU1分別在兩個(gè)獨(dú)立的CPU上執(zhí)行。假如 CPU0 的緩存行中緩存了 isFinish 這個(gè)共享變量,并且狀態(tài)為(E)、而 Value可能是(S)狀態(tài)。

文章圖片9

那么這個(gè)時(shí)候,CPU0 在執(zhí)行的時(shí)候,會(huì)先把 value=10的指令寫入到storebuffer中。并且通知給其他緩存了該value變量的 CPU。在等待其他 CPU通知結(jié)果的時(shí)候,CPU0會(huì)繼續(xù)執(zhí)行 isFinish=true這個(gè)指令。

而因?yàn)楫?dāng)前 CPU0緩存了 isFinish并且是 Exclusive狀態(tài),所以可以直接修改 isFinish=true。這個(gè)時(shí)候 CPU1 發(fā)起 read操作去讀取 isFinish 的值可能為 true,但是 value的值不等于10。

這種情況我們可以認(rèn)為是 CPU的亂序執(zhí)行,也可以認(rèn)為是一種重排序,而這種重排序會(huì)帶來可見性的問題volatile 關(guān)鍵字?或者說為什么還會(huì)存在可見性問題呢?

這下硬件工程師也抓狂了,我們也能理解,從硬件層面很難去知道軟件層面上的這種前后依賴關(guān)系,所以沒有辦法通過某種手段自動(dòng)去解決。

所以硬件工程師就說: 既然怎么優(yōu)化都不符合你的要求,要不你來寫吧。

所以在 CPU 層面提供了 memory barrier(內(nèi)存屏障)的指令,從硬件層面來看這個(gè) memroy barrier 就是 CPU flush store bufferes中的指令。軟件層面可以決定在適當(dāng)?shù)牡胤絹聿迦雰?nèi)存屏障。

CPU 層面的內(nèi)存屏障

什么是內(nèi)存屏障?從前面的內(nèi)容基本能有一個(gè)初步的猜想,內(nèi)存屏障就是將 store bufferes 中的指令寫入到內(nèi)存,從而使得其他訪問同一共享內(nèi)存的線程的可見性。

X86的memory barrier指令包括 lfence(讀屏障) sfence(寫屏障) mfence(全屏障)Store Memory Barrier(寫屏障) 告訴處理器在寫屏障之前的所有已經(jīng)存儲(chǔ)在存儲(chǔ)緩存(store bufferes)中的數(shù)據(jù)同步到主內(nèi)存,簡(jiǎn)單來說就是使得寫屏障之前的指令的結(jié)果對(duì)屏障之后的讀或者寫是可見的

Load Memory Barrier(讀屏障) 處理器在讀屏障之后的讀操作,都在讀屏障之后執(zhí)行。配合寫屏障,使得寫屏障之前的內(nèi)存更新對(duì)于讀屏障之后的讀操作是可見的

Full Memory Barrier(全屏障) 確保屏障前的內(nèi)存讀寫操作的結(jié)果提交到內(nèi)存之后,再執(zhí)行屏障后的讀寫操作有了內(nèi)存屏障以后,對(duì)于上面這個(gè)例子,我們可以這么來改,從而避免出現(xiàn)可見性問題

文章圖片10

總的來說,內(nèi)存屏障的作用可以通過防止 CPU對(duì)內(nèi)存的亂序訪問來保證共享數(shù)據(jù)在多線程并行執(zhí)行下的可見性但是這個(gè)屏障怎么來加呢?回到最開始我們講 volatile 關(guān)鍵字的代碼,這個(gè)關(guān)鍵字會(huì)生成一個(gè) Lock的匯編指令,這個(gè)指令其實(shí)就相當(dāng)于實(shí)現(xiàn)了一種內(nèi)存屏障這個(gè)時(shí)候問題又來了,內(nèi)存屏障、重排序這些東西好像是和平臺(tái)以及硬件架構(gòu)有關(guān)系的。作為 Java 語言的特性,一次編寫多處運(yùn)行。我們不應(yīng)該考慮平臺(tái)相關(guān)的問題,并且這些所謂的內(nèi)存屏障也不應(yīng)該讓程序員來關(guān)心。

文章圖片11

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多