1. 程式人生 > >樂觀鎖悲觀鎖,CAS,volatile

樂觀鎖悲觀鎖,CAS,volatile

悲觀鎖

假設最壞的情況,每次去讀取資料都認為別人會修改資料,所以可能會產生併發,於是每次在拿資料的時候都要上鎖。Java裡面的同步原語synchronized關鍵字的實現也是悲觀鎖。

樂觀鎖

就是每次拿資料的時候認為不會有人來修改資料,所以不上鎖,但是在更新的時候會判斷此期間是否有人去修改了鎖,如果併發衝突那麼就重試,直到成功為止。

CAS

樂觀鎖是一種思想,而CAS是一種機制,通過CAS(Compare and Swap)可以實現樂觀鎖。
而實現樂觀鎖的兩個步驟就是
1.衝突檢測
2.資料更新

以上的具體問題。。。。還有待研究
見文章https://www.cnblogs.com/qjjazry/p/6581568.html

volatile

https://www.cnblogs.com/dolphin0520/p/3920373.html
總結一些核心的思想。。。具體實現還是要多用

總結的說
原理:volatile是可以保證資料的可見性,即當對一個volatile資料進行修改的時候,可以保證此時快取行的進行讀入,並且立即將修改的值從自己的快取中寫入主存,快取行會等待主存地址更改的通知後,才會讀入資料


原理導致的特性:以通過volatile關鍵字來保證一定的“有序性”,即volatile保證對一個變數的寫操作先行發生於後面對這個變數的讀操作。可以保證讀取到最新的資料,但是不能保證變數自增的原子性。

1.為了提高CPU利用率,每個執行緒(CPU)都會有自己的快取,但是由於快取互相不可見可能會導致資料的髒讀等等

要理解volatile,首先要理解一些概念,下面簡單介紹下

由於CPU讀取速度遠遠快於主存的讀寫速度,為了提高CPU利用的效率,設計了快取記憶體。
舉個例子

i = i + 1;

這個語句在執行的時候,首先將i的值從主存中讀入到快取記憶體中,然後CPU執行+1的指令,再將結果返回給主存。

單執行緒執行沒有任何問題,但是多核CPU中,每條執行緒可能運行於不同的CPU中,因此每個執行緒執行時有自己的快取記憶體(對單核CPU來說,其實也會出現這種問題,只不過是以執行緒排程的形式來分別執行的)。

比如同時有2個執行緒執行這段程式碼,假如初始時i的值為0,那麼我們希望兩個執行緒執行完之後i的值變為2。但是事實會是這樣嗎?

可能存在下面一種情況:初始時,兩個執行緒分別讀取i的值存入各自所在的CPU的快取記憶體當中,然後執行緒1進行加1操作,然後把i的最新值1寫入到記憶體。此時執行緒2的快取記憶體當中i的值還是0,進行加1操作之後,i的值為1,然後執行緒2把i的值寫入記憶體。

最終結果i的值是1,而不是2。這就是著名的快取一致性問題。通常稱這種被多個執行緒訪問的變數為共享變數

也就是說,如果一個變數在多個CPU中都存在快取也可以理解為一個變數被多個執行緒共享),那麼就可能存在快取不一致的問題。

為了解決快取不一致性問題,通常來說有以下2種解決方法:
1)硬體層面 ,對CPU匯流排加鎖,但是浪費CPU資源
2)快取一致性協議

快取一致性定理:它核心的思想是:當CPU寫資料時,如果發現操作的變數是共享變數,即在其他CPU中也存在該變數的副本,會發出訊號通知其他CPU將該變數的快取行置為無效狀態,因此當其他CPU需要讀取這個變數時,發現自己快取中快取該變數的快取行是無效的,那麼它就會從記憶體重新讀取。

2.併發程式設計中的三個概念

1).原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
2).可見性:可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。
例子:

//執行緒1執行
int i = 0;
i = 10;

//執行緒2執行
j = i;

假若執行執行緒1的是CPU1,執行執行緒2的是CPU2。由上面的分析可知,當執行緒1執行 i =10這句時,會先把i的初始值載入到CPU1的快取記憶體中,然後賦值為10,那麼在CPU1的快取記憶體當中i的值變為10了,卻沒有立即寫入到主存當中。

此時執行緒2執行 j = i,它會先去主存讀取i的值並載入到CPU2的快取當中,注意此時記憶體當中i的值還是0,那麼就會使得j的值為0,而不是10.

這就是可見性問題,執行緒1對變數i修改了之後,執行緒2沒有立即看到執行緒1修改的值。

3).有序性:
處理器為了提高程式執行效率,可能會對輸入程式碼進行優化,它不保證程式中各個語句的執行先後順序同程式碼中的順序一致,但是它會保證程式最終執行結果和程式碼順序執行的結果是一致的。
可指令重排序雖然不會影響單個執行緒的執行,但是會影響到執行緒併發執行的正確性。

也就是說,要想併發程式正確地執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程式執行不正確。

JAVA對於以上三點的保證
1.首先基本操作的賦值讀取是原子性的,而對於更大範圍的原子性,則用sychronized和Lock來保證
2.JAVA提供了volatile來保證可見性。當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。

另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個執行緒獲取鎖然後執行同步程式碼,並且在釋放鎖之前會將對變數的修改重新整理到主存當中。因此可以保證可見性。

3.有序性:以通過volatile關鍵字來保證一定的“有序性”,即volatile保證對一個變數的寫操作先行發生於後面對這個變數的讀操作。

另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個執行緒執行同步程式碼,相當於是讓執行緒順序執行同步程式碼,自然就保證了有序性。

3.volatile

1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。

2)禁止進行指令重排序。

//執行緒1
boolean stop = false;
while(!stop){
    doSomething();
}

//執行緒2
stop = true;

這個while是否會跳出來呢,有可能,也可能不跳出來

原因在於:執行緒2改變stop的值之後,還沒來得及寫入主存,就去做別的事情了,導致執行緒1看不到執行緒2的改變。

但是volatile關鍵字的作用:

第一:使用volatile關鍵字會強制將修改的值立即寫入主存;

第二:使用volatile關鍵字的話,當執行緒2進行修改時,會導致執行緒1的工作記憶體中快取變數stop的快取行無效效);

第三:由於執行緒1的工作記憶體中快取變數stop的快取行無效,所以執行緒1再次讀取變數stop的值時會去主存讀取。

所以說當執行緒2改變自己快取中變數的值的時候,會將值立即寫入主存,且保證執行緒1中的快取行無效,直到等待主存地址被更新後,才會去主存讀入值。

但是
volatile關鍵字能保證可見性沒有錯。可見性只能保證每次讀取的是最新的值,

但是volatile沒辦法保證對變數的操作的原子性。

例如inc++這個自增語句,包括讀取變數的原始值、進行加1操作、寫入工作記憶體
,就有可能導致下面這種情況出現:

假如某個時刻變數inc的值為10,

執行緒1對變數進行自增操作,執行緒1先讀取了變數inc的原始值,然後執行緒1被阻塞了;

然後執行緒2對變數進行自增操作,執行緒2也去讀取變數inc的原始值,由於執行緒1只是對變數inc進行讀取操作,而沒有對變數進行修改操作,所以不會導致執行緒2的工作記憶體中快取變數inc的快取行無效,所以執行緒2會直接去主存讀取inc的值,發現inc的值時10,然後進行加1操作,並把11寫入工作記憶體,最後寫入主存。

有序性:

context = loadContext();   //語句1
inited = true;             //語句2

//執行緒2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

前面舉這個例子的時候,提到有可能語句2會在語句1之前執行,那麼久可能導致context還沒被初始化,而執行緒2中就使用未初始化的context去進行操作,導致程式出錯。

這裡如果用volatile關鍵字對inited變數進行修飾,就不會出現這種問題了,因為當執行到語句2時,必定能保證context已經初始化完畢。