biang崽學(xué)java (1)—— 并發(fā)安全問題

1.概念

1.進(jìn)程

一個(gè)程序運(yùn)行的基本單位。包括計(jì)算機(jī)全部資源 + 程序。【cpu、內(nèi)存、外設(shè)】

2.線程

實(shí)現(xiàn)cpu的虛擬化。同一個(gè)進(jìn)程下的線程共享內(nèi)存,但是每個(gè)線程有獨(dú)立的棧 <=> 看起來好像有一個(gè)自己的cpu。【會(huì)將cpu的上下文內(nèi)容儲(chǔ)存在棧中】

【之所以出現(xiàn)線程是因?yàn)椋阂粋€(gè)程序可能需要實(shí)現(xiàn)不同的功能,或者需要同時(shí)處理多個(gè)任務(wù),但是又需要這些功能共享內(nèi)存,如果創(chuàng)建多個(gè)進(jìn)程,開銷會(huì)很大,而且進(jìn)程通信的代價(jià)高,因此輕便的多線程由民間走進(jìn)官方視野】

3.并發(fā)

是指同時(shí)開始,但是每一個(gè)時(shí)刻做的只有一個(gè),每一個(gè)做一個(gè)小時(shí)間片?!締魏硕嗑€程就可以】

4.并行

同時(shí)運(yùn)行。在一個(gè)時(shí)刻會(huì)有多個(gè)在同時(shí)執(zhí)行?!具@個(gè)需要多核來實(shí)現(xiàn)】

進(jìn)程中的多個(gè)線程就是并發(fā)執(zhí)行的,但是由于他們共享資源,就產(chǎn)生了一些安全問題。

image一個(gè)具體場景:

假設(shè)內(nèi)存中有一個(gè)變量num = 0;線程AB對其做同樣的工作,對num++線程A將A拿出++之后,時(shí)間片用盡;開始執(zhí)行線程B,也取出num++;這時(shí)應(yīng)當(dāng)發(fā)現(xiàn)問題因?yàn)榫€程A操作完之后沒有講數(shù)據(jù)還回內(nèi)存,線程B就打斷了它,這是線程B看到并操作的num = 0;相當(dāng)于A、B都執(zhí)行的是將num 從0 變成1。之后可以判斷,A將1寫回,B也將1寫回內(nèi)存。最終num?= 1。

5.線程安全問題/臨界區(qū)問題

1)問題產(chǎn)生原因剖析:

線程對資源產(chǎn)生爭奪。在爭奪過程中被別的線程打斷了執(zhí)行。修改值包含取出——修改——寫回3步。取出、修改執(zhí)行完被打斷,還沒寫回更新就被打斷了,這個(gè)時(shí)候內(nèi)存的值已經(jīng)不是最新的了,但是其他線程并不知道。

=>需要保證爭奪資源的這部分代碼變成原子化,eg:取出——修改——寫回一塊執(zhí)行完而不能被其他線程打斷。

2)解決線程安全問題的方法

加鎖的思想 —— 將操作原子化

? ? ?(1)同步代碼塊 synchronized

? ? ?(2)同步方法

? ? ?(3)lock變量【c++直接用std::mutex】

純純將操作原子化

? ? ?(1)CAS操作




2.鎖機(jī)制

1.實(shí)現(xiàn)方法:

同步代碼塊

synchronized (監(jiān)視器) {

? ? 被鎖起來的代碼

}

1.監(jiān)視器一定是唯一的,可以被所有線程看到的,否則不能實(shí)現(xiàn)執(zhí)行這段代碼時(shí)只有一個(gè)線程在執(zhí)行不被打斷(因?yàn)槠渌€程可能看不到鎖)

2.任何一個(gè)對象都可以做監(jiān)聽器

同步方法

provide synchronized void 函數(shù)名(){

? ? 函數(shù)體

}

1.可以在implement Runable接口中重寫run實(shí)現(xiàn)使用

手動(dòng)加鎖,釋放鎖

privateLocklock=newReentrantLock();//創(chuàng)建一個(gè)lock對象,因?yàn)長ock是一個(gè)接口 注意new的是ReentrantLock,而不是Lock

……

lock.lock();

被鎖起來的代碼

lock.unlock()

1.lock效率比較低

2.手動(dòng)釋放鎖的時(shí)候很有可能會(huì)忘記釋放建議使用try{ 上鎖代碼 }finall{ lock.unlock}

try{

? ? lock.lock();

? ? ……

}

finally{

? ? lock.unlock();

}

3.如果忘記釋放鎖會(huì)導(dǎo)致上鎖之后部分的代碼一直處于單線程狀態(tài),別的線程無法執(zhí)行,效率低下。


2.上鎖機(jī)制

在之前的文章中有總結(jié)過一部分較為詳細(xì)。接下來我完善一些細(xì)節(jié)。

之前的總結(jié)


首先我們清楚了?synchronized 的實(shí)現(xiàn)歷程從無鎖 <—— 偏向鎖 <—— 輕量鎖 <—— 重量級鎖。以及產(chǎn)生的大致緣由:下面詳細(xì)解釋這幾種鎖以及此時(shí)當(dāng)一個(gè)線程來爭奪鎖的流程細(xì)節(jié)。

講鎖之前先要熟悉一個(gè)概念 —— CAS操作

CAS:【compare and swap 比較交換】。實(shí)現(xiàn)給某個(gè)內(nèi)存原子化換值【之前都是取出——修改——寫回,不安全】。

該操作保存:一個(gè)內(nèi)存地址addr,之前保留的這個(gè)地址存儲(chǔ)的值old_value,要修改成的新值new_value。

操作:比較addr這個(gè)地址內(nèi)存儲(chǔ)的值和我之前存的old_value值是否還保持一致,如果一樣就把它換成new_value。不一樣就會(huì)放棄操作(不一樣可能是因?yàn)橛芯€程爭奪到并修改了值)。這下只寫new_value是原子的。


重量級鎖

我們熟悉的鎖操作,當(dāng)線程爭奪資源時(shí),發(fā)現(xiàn)資源被其他線程上了鎖,就要被阻塞等待,直到當(dāng)前線程釋放鎖被喚醒重新爭奪鎖。

其底層通過監(jiān)視器鎖(monitor) —— OS互斥鎖(mutex)實(shí)現(xiàn)。

輕量級鎖

如果一爭奪就要去睡,然后被喚醒這樣需要切換到kernel去執(zhí)行,頻繁地切換會(huì)消耗大量資源(浪費(fèi)時(shí)間、CPU等),如果是很快就釋放了鎖豈不是代價(jià)太大了,因此提出輕量級鎖。當(dāng)我爭奪資源的時(shí)候,如果發(fā)現(xiàn)被鎖了,我先等等看,一定時(shí)間后再查看一下是否釋放了鎖,如果釋放了則參與爭奪;如果這個(gè)等待時(shí)間短于我阻塞切換的時(shí)間,就贏了。

偏向鎖

這樣等等,等不到再去阻塞還不夠,當(dāng)只有一個(gè)線程反復(fù)爭奪鎖的時(shí)候還可以繼續(xù)優(yōu)化,反正只有一個(gè)線程爭奪鎖,就不要再加鎖機(jī)制了,就記錄下線程id,下一次來再獲取的時(shí)候,對比看看是不是還是當(dāng) 前線程線程 上次擁有的資源,如果是就直接執(zhí)行,如果不是說明這個(gè)資源已經(jīng)被爭奪了。那就立刻升級成為輕量級鎖。

無鎖

當(dāng)一個(gè)線程沒有要爭奪的資源時(shí)是處于無鎖狀態(tài)的,此時(shí)所有線程都可以獲取這個(gè)資源。

于是當(dāng)一個(gè)進(jìn)程被執(zhí)行的時(shí)候就會(huì)走下面的流程:

1.一個(gè)進(jìn)程上鎖會(huì)在線程的棧幀里創(chuàng)建lockRecord,在lockRecord里和鎖對象的MarkWord里存儲(chǔ)線程a的線程id。一個(gè)線程執(zhí)行:CAS操作測試一下對象頭的Mark Word里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖,成功就是當(dāng)前線程上鎖(偏向鎖)【可能情況是:當(dāng)前線程號為null,或者線程號一致】;否則就是另一個(gè)線程鎖著,檢查這個(gè)原來持有該對象鎖的線程是否依然存活,掛了,則可以將對象變?yōu)闊o鎖狀態(tài),然后重新偏向新的線程;存活則鎖升級到輕量級鎖。

2.在執(zhí)行同步塊之前,JVM會(huì)先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中,然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖。如果,完成自旋策略還是發(fā)現(xiàn)線程沒有釋放鎖,或者讓別的線程占用,則線程試圖將輕量級鎖升級為重量級鎖。

ps:升級為重量級鎖有兩種情況,

一個(gè)是自旋到了一定次數(shù);

另一個(gè)是第一個(gè)線程上鎖,第二個(gè)線程自旋,第三個(gè)線程來的時(shí)候就轉(zhuǎn)化為重量級鎖。

3.自旋的線程由用戶態(tài)切換到內(nèi)核態(tài)進(jìn)行阻塞,等待之前線程執(zhí)行完成,內(nèi)核喚醒等待在這個(gè)鎖上的線程進(jìn)行競爭。

有一個(gè)大佬博主畫了十分詳細(xì)的流程圖,

大佬總結(jié)流程圖

還有《深入理解Java虛擬機(jī):JVM高級特性與最佳實(shí)踐(第2版)》中的簡化版流程圖:


簡化流程圖

3.其他鎖機(jī)制

鎖剔除:一定不會(huì)發(fā)生線程安全問題,其他線程不會(huì)操作數(shù)據(jù)?!恍枰i

鎖粗化:對同一個(gè)對象反復(fù)加鎖、釋放鎖?!獙㈡i的范圍擴(kuò)展到整個(gè)操作序列外部。


3.CAS操作

前面大概講解了CAS操作的實(shí)現(xiàn)機(jī)制,我們發(fā)現(xiàn)通過比較-交換就很好地實(shí)現(xiàn)了原子化操作,那不用鎖機(jī)制,直接使用CAS操作不就可以了嗎?

循環(huán)CAS操作:每次先取出舊值,然后比較取出的值和當(dāng)前內(nèi)存的值是否一致,一致操作;不一致循環(huán)檢測。

而且通過CAS比較交換可以擴(kuò)展實(shí)現(xiàn)一群原子化操作:

1.自增/自減 :新值變?yōu)榕f值+1 / 舊值-1?

2.交換:新值變?yōu)橐粨Q的值。

3.也可以不相等則……

但是cas真的可以這么完美嗎?

cas存在的問題:

1.ABA:CAS根本在于想要查看值是否發(fā)生了變化來決定是否執(zhí)行這次操作。檢查變化時(shí)直接用比較實(shí)現(xiàn)的??墒侵等耘f相等并不代表沒有發(fā)生過變化:當(dāng)當(dāng)前線程比較之前,有一個(gè)線程將這個(gè)值改回去了,然后當(dāng)前這個(gè)線程以為這個(gè)值沒有修改過就繼續(xù)執(zhí)行了后續(xù)操作。

解決這問題的方法:是為每一次操作引入一個(gè)version號,操作一次++,以此來區(qū)別不同操作。

2.循環(huán)檢測CAS是否可以執(zhí)行,如果一直不能實(shí)現(xiàn)操作就會(huì)造成CPU資源開銷浪費(fèi)。

3.一次循環(huán)是檢測一個(gè)共享變量的,如果是多個(gè)共享變量就難以支持?!緯r(shí)間拉的太長?】



目前各類語言提供的線程安全的變量:對其增減都是原子化的:

std::atomic【c++】

AtomicInteger【java】


4.volatile關(guān)鍵字

并發(fā)編程的3大特性

1)原子性:某些操作必須是一個(gè)整體,不可以被打斷。

2)可見性:一個(gè)線程改變了值要保證其他線程都可以看見(知曉)。

3)有序性:優(yōu)化進(jìn)行指令重排,對于單線程是串行,對于多線程就變得無序。


1.volatile原理

volatile實(shí)現(xiàn)的就是可見性和有序性。

實(shí)現(xiàn)機(jī)制

主線程有一個(gè)主存,其他線程都將主存中的變量拷貝一份到本地線程,互不干擾,當(dāng)有一個(gè)線程修改volatile修飾的變量時(shí),就會(huì)將該值強(qiáng)制刷新到主存,并導(dǎo)致其他線程的緩存無效。當(dāng)其他線程想要使用該變量時(shí),發(fā)現(xiàn)值無效就從主存中重新獲取最新變量。


實(shí)現(xiàn)的底層原理

volatile作為關(guān)鍵字修飾變量。在這個(gè)變量的讀寫操作前后,加一些屏障控制,可以實(shí)現(xiàn):

當(dāng)volatile變量被讀取前,讀取操作執(zhí)行完畢;這個(gè)volatile變量讀取操作完成之后才可以進(jìn)行其它讀寫操作;

這次寫操作執(zhí)行之前的寫操作都被線程看到;當(dāng)前寫操作之后的所有讀操作都看到了這次寫過程。

在jvm上實(shí)現(xiàn)了4種屏障:

LoadLoad屏障: 對于這樣的語句Load1; LoadLoad; Load2,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。

StoreStore屏障:對于這樣的語句Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見。

LoadStore屏障:對于這樣的語句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。

StoreLoad屏障: 對于這樣的語句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對所有處理器可見。


硬件層面提供的屏障

sfence:即寫屏障(Store Barrier),在寫指令之后插入寫屏障,能讓寫入緩存的最新數(shù)據(jù)寫回到主內(nèi)存,以保證寫入的數(shù)據(jù)立刻對其他線程可見

lfence:即讀屏障(Load Barrier),在讀指令前插入讀屏障,可以讓高速緩存中的數(shù)據(jù)失效,重新從主內(nèi)存加載數(shù)據(jù),以保證讀取的是最新的數(shù)據(jù)。

mfence:即全能屏障(modify/mix Barrier ),兼具sfence和lfence的功能

lock 前綴:lock不是內(nèi)存屏障,而是一種鎖。執(zhí)行時(shí)會(huì)鎖住內(nèi)存子系統(tǒng)來確保執(zhí)行順序,甚至跨多個(gè)CPU。


volatile的屏障:

LoadLoadBarrier
volatile 讀操作
LoadStoreBarrier

StoreStoreBarrier
volatile 寫操作
StoreLoadBarrier

源碼——jdk8:寫操作

在底層源碼實(shí)現(xiàn)的時(shí)候,可以看到首先判斷了這個(gè)變量的類型是否為volatile,如果是則在賦值操作前后添加屏障,否則就直接賦值。


2.volatile實(shí)現(xiàn)了什么

上面已經(jīng)講了volatile實(shí)現(xiàn)的是可見性和有序性,通過屏障機(jī)制嚴(yán)格限制某些指令執(zhí)行的順序,并且在變量發(fā)生變化之后及時(shí)使其他線程本地變量無效,實(shí)現(xiàn)了可見性。

可是他并不能保證原子性操作,即獲取——修改——寫回之間還是可以被打斷的,因此無法保證線程安全。

詳細(xì)分析:

還是兩個(gè)線程A、B想要實(shí)現(xiàn)對主存中的num++,num被volatile修飾。

A、B均保存了當(dāng)前變量到本地線程,當(dāng)線程A加載 num到寄存器中,時(shí)間片用完被打斷,此時(shí)還沒有寫回到本地變量,沒有觸發(fā)寫屏障;

線程B 加載 num到寄存器實(shí)現(xiàn)++,并將其寫回到本地變量,然后觸發(fā)寫屏障,強(qiáng)制刷新到主存;

回到線程A, 這時(shí)雖然之前B線程觸發(fā)了寫屏障,但是A線程在此之前已經(jīng)load了num到寄存器,沒有對其的訪問了,因此并不會(huì)再次從主存中獲取最新數(shù)據(jù),而是將寄存器的數(shù)據(jù)++之后寫回到變量,也觸發(fā)寫屏障,強(qiáng)制寫回。

因此線程仍舊不安全。

大佬文章:

分析鎖原理以及變化路徑↓

淺談偏向鎖、輕量級鎖、重量級鎖 - 簡書


講CAS操作 & CAS如何實(shí)現(xiàn)線程安全↓

深入理解CAS操作 - 知乎

CAS操作_姑娘加油的博客-CSDN博客_cas操作


volatile底層原理 扒了源碼 & 詳細(xì)分析了volatile為啥還會(huì)造成線程不安全↓

volatile底層原理詳解 - 知乎

為什么volatile也無法保證線程安全_IT_農(nóng)廠的博客-CSDN博客_volatile為什么不能保證線程安全

volatile關(guān)鍵字無法保證線程安全的討論_Simon銘少的博客-CSDN博客_volatile關(guān)鍵字不能保證線程安全


哦哦哦現(xiàn)在應(yīng)該對于線程安全這個(gè)問題有了一定程度的了解了,有很多細(xì)節(jié)還需要扣扣源碼和細(xì)節(jié),

例如

原子化關(guān)鍵字操作是否要配合CAS才能實(shí)現(xiàn)線程安全?

原子化關(guān)鍵字是如何實(shí)現(xiàn)線程安全的?

……

共勉呀~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容