農林漁牧網

您現在的位置是:首頁 > 農業

「高頻面試」鎖與CAS詳解(建議收藏)

2022-07-14由 紙鶴視界 發表于 農業

原子資料測試結果準確嗎

【高頻面試】鎖與CAS詳解

大家好,我是java廠長,今天帶你們瞭解高頻面試之Java鎖!

作者介紹

部落格主頁:作者主頁

簡介:JAVA領域優質創作者?、一名在校大三學生、在校期間參加各種省賽、國賽,斬獲一系列榮譽。

關注我:關注我學習資料、文件下載統統都有,每日定時更新文章,勵志做一名JAVA資深程式猿。

文章目錄

【高頻面試】鎖與CAS詳解

一。 悲觀鎖與樂觀鎖

二、實現方式

1)CAS(Compare And Swap)

2)版本號機制

三、面試官問:樂觀鎖加鎖嗎?

四、面試官問:CAS有哪些缺點?

1)一次性只能保證一個共享變數的原子性

2)迴圈會耗時

3)存在ABA問題(重點)

五、適用場景

1)功能限制

2)競爭激烈程度

此篇博文對Java學習理解底層很有幫助!

一。 悲觀鎖與樂觀鎖

樂觀鎖和悲觀鎖問題,是出現頻率比較高的面試題。本文將由淺入深,逐步介紹它們的基本概念、實現方式(含例項)、適用場景,以及可能遇到的面試官追問,希望能夠幫助你打動面試官。

樂觀鎖和悲觀鎖是兩種思想,主要是解決併發場景下的資料爭奪的問題。

樂觀鎖:樂觀鎖在操作資料時非常樂觀,認為別人不會同時修改資料。因此樂觀鎖不會上鎖,只是在執行更新的時候判斷一下在此期間別人是否修改了資料:如果別人修改了資料則放棄操作,否則執行操作。

悲觀鎖:悲觀鎖在操作資料時比較悲觀,認為別人會同時修改資料。因此操作資料時直接把資料鎖住,直到操作完成後才會釋放鎖;上鎖期間其他人不能修改資料。

二、實現方式

悲觀鎖的實現方式是加鎖,加鎖既可以是對程式碼塊加鎖(如Java的synchronized關鍵字),也可以是對資料加鎖(如MySQL中的排它鎖)。

樂觀鎖的實現方式有兩種:CAS機制和版本號機制。

1)CAS(Compare And Swap)

CAS的原理很簡單,包含三個值

需要讀寫的記憶體位置(V)

預期原來的值(A)

期待更新的值(B)。

如果記憶體位置V的值與預期原值A相匹配,那麼處理器會自動將該位置值更新為新值B,返回true。否則處理器不做任何操作,返回false。

實現CAS最重要的一點,就是比較和交換操作的一致性,否則就會產生歧義。

CAS操作邏輯如下:如果記憶體位置V的值等於預期的A值,則將該位置更新為新值B,否則不進行任何操作。許多CAS的操作是自旋的:如果操作不成功,會一直重試,直到操作成功為止。

這裡引出一個新的問題,既然CAS包含了Compare和Swap兩個操作,它又如何保證原子性呢?答案是:CAS是由CPU支援的原子操作,其原子性是在硬體層面進行保證的。

比如當前執行緒比較成功後,準備更新共享變數值的時候,這個共享變數值被其他執行緒更改了,那麼CAS函式必須返回false。

要實現這個需求,java中提供了Unsafe類,它提供了三個函式,分別用來操作基本型別int和long,以及引用型別Object。

「高頻面試」鎖與CAS詳解(建議收藏)

引數的意義:

var1和 var2:表示這個共享變數的記憶體地址。這個共享變數是var1物件的一個成員屬性,var2表示這個共享變數在var1類中的記憶體偏移量。所以透過這兩個引數就可以直接在記憶體中修改和讀取共享變數值。

var4: 表示預期原來的值。

var5: 表示期待更新的值。

併發比較低的時候用CAS比較合適,併發比較高用synchronized比較合適。

接下來以Java中的自增操作( i++ )為例,看一下悲觀鎖和CAS分別是如何保證執行緒安全的。在Java中自增操作不是原子操作,它實際上包含三個獨立的操作:第一步是讀取i值;第二步是加1;第三步是將新值賦值給i

package

com

zmz

lock

import

java

util

concurrent

atomic

AtomicInteger

/** * @ProjectName: Juc * @Package: com。zmz。lock * @ClassName: LockTest * @Author: 張晟睿 * @Date: 2021/10/17 14:50 * @Version: 1。0 */

public

class

LockTest

{

//執行緒不安全

private

static

int

num1

=

0

//使用樂觀鎖

private

static

AtomicInteger

num2

=

new

AtomicInteger

0

//使用悲觀鎖

private

static

int

num3

=

0

private

static

synchronized

void

addNum3

{

num3

++

}

public

static

void

main

String

args

throws

Exception

{

//開啟2000個執行緒 自增

for

int

i

=

0

i

<

2000

i

++

{

new

Thread

new

Runnable

{

@Override

public

void

run

{

try

{

Thread

sleep

200

}

catch

InterruptedException

e

{

e

printStackTrace

}

num1

++

num2

getAndIncrement

addNum3

}

}

start

}

Thread

sleep

2000

//休眠2s

System

out

println

“1、執行緒不安全:”

+

num1

System

out

println

“2、樂觀鎖(AtomicInteger):”

+

num2

System

out

println

“3、悲觀鎖(synchronized):”

+

num3

}

}

「高頻面試」鎖與CAS詳解(建議收藏)

透過實驗,我們發現併發執行自增操作,導致計算結果的不準確。在上面的程式碼測試中:num1沒有進行任何執行緒安全方面的保護,num2使用了樂觀鎖(CAS),num3使用了悲觀鎖(synchronized)。執行程式,使用2000個執行緒同時對num1、num2和num3進行自增操作,可以發現:num2和num3的值總是等於2000,而num1的值常常小於2000。

首先來介紹

AtomicInteger

。AtomicInteger是java。util。concurrent。atomic包提供的原子類,利用CPU提供的CAS操作來保證原子性;這個包裡面提供了一組原子變數類。其基本的特性就是在多執行緒環境下,當有多個執行緒同時執行這些類的例項包含的方法時,具有排他性,即當某個執行緒進入方法,執行其中的指令時,不會被其他執行緒打斷,而別的執行緒就像自旋鎖一樣,一直等到該方法執行完成,才由JVM從等待佇列中選擇一個另一個執行緒進入,這只是一種邏輯上的理解。實際上是藉助硬體的相關指令來實現的,不會阻塞執行緒(或者說只是在硬體級別上阻塞了)。可以對基本資料、陣列中的基本資料、對類中的基本資料進行操作。原子變數類相當於一種泛化的volatile變數,能夠支援原子的和有條件的讀-改-寫操作。除了AtomicInteger外,還有

AtomicBoolean、AtomicLong、AtomicReference

等眾多原子類。

下面看一下AtomicInteger的原始碼,瞭解下它的自增操作getAndIncrement()是如何實現的(JAVA8)

「高頻面試」鎖與CAS詳解(建議收藏)

public

class

AtomicInteger

extends

Number

implements

java

io

Serializable

{

private

static

final

long

serialVersionUID

=

6214790243416807050L

//Unsafe用於實現對底層資源的訪問

private

static

final

Unsafe

unsafe

=

Unsafe

getUnsafe

//valueOffset是value在記憶體中的偏移量

private

static

final

long

valueOffset

/** * 透過Unsafe獲得valueOffset * Unsafe類是用來在任意記憶體地址位置處讀寫資料,可見,對於普通使用者來說,使用起來還是比較危險的。 * public native long objectFieldOffset(Field var1);方法用於獲取某個欄位相對Java物件的“起始地址”的偏移量,方法返回值和引數如下 * AtomicInteger。class。getDeclaredField(“value”)是拿到atomicInteger的value欄位的field物件 * valueoffset是拿到value的相對於AtomicInteger物件的地址偏移量 * */

static

{

try

{

valueOffset

=

unsafe

objectFieldOffset

AtomicInteger

class

getDeclaredField

“value”

}

catch

Exception

ex

{

throw

new

Error

ex

}

}

/** * 以原子方式將值設定為給定的更新值 * * @param expect 預期值 * @param update 新值 * @return 成功返回true 返回false表明實際值不等於預期值,設定失敗 */

public

final

boolean

compareAndSet

int

expect

int

update

{

return

unsafe

compareAndSwapInt

this

valueOffset

expect

update

}

/** * 原子上增加一個當前值。 * * @return 之前的值 */

public

final

int

getAndIncrement

{

return

unsafe

getAndAddInt

this

valueOffset

1

}

/* getAndAddInt函式的方法體,傳進來的var4是1,每呼叫一次增加1。compareAndSwapInt前面解釋過了 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this。getIntVolatile(var1, var2); } while(!this。compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } */

}

2)版本號機制

版本號機制也可以用來實現樂觀鎖。版本號機制的主要思想是在資料中增加一個欄位version,表示該資料的版本號,每當資料被修改時,同時讀取版本號version的值,若剛才讀取到的version值為當前資料庫中的version值相等時才更新,則版本號加1;否則重試更新操作,直到更新成功。當某個執行緒查詢資料時,將該資料的版本號一起查出來;當該執行緒更新資料時,判斷當前版本號與之前讀取的版本號是否一致,如果一致才進行操作。

舉一個簡單的銀行取錢的例子:

假設資料庫中帳戶資訊表中有一個 version 欄位,當前值為 1 ;而當前帳戶餘額欄位( balance )為 $100 。

操作員 A 此時讀出版本號( version=1 ),並從其帳戶餘額中扣除 $50( $100-$50 )。

接下來在操作員 A 操作的過程中,操作員B 也讀入此餘額及版本號( version=1 ),並從其帳戶餘額中扣除 $30 ( $100-$30 )。

操作員 A 完成了修改工作,將資料版本號加一,此時版本號( version=2 )、帳戶餘額( balance=$50 ),提交至資料庫更新,此時,提交資料版本 > 資料庫記錄當前版本,資料被更新,並且資料庫記錄 version 更新為 2 。

操作員 B 完成了操作,也將版本號+1( version=2 )試圖向資料庫提交資料( balance=$70 ),但此時比對資料庫記錄版本時發現,操作員 B 提交的資料版本號為 2 ,資料庫記錄當前版本也為 2 ,不滿足 “ 當前最後更新的version與操作員第一次的版本號相等 “ 的樂觀鎖策略,因此,操作員 B 的提交被駁回。

這樣,就避免了操作員 B 用基於 version=1 的舊資料修改的結果覆蓋操作員A 的操作結果的可能。

三、面試官問:樂觀鎖加鎖嗎?

在面試時,曾遇到面試官如此追問。下面是我對這個問題的理解:

(1)樂觀鎖本身是不加鎖的,只是在更新資料的時候會判斷一下資料是否被其他執行緒已經更新過了

(2)有時樂觀鎖可能與加鎖操作兩者同時使用

四、面試官問:CAS有哪些缺點?

面試到這裡,我可能就要恭喜你大機率是面試通過了????,面試官可能已經中意你了。不過面試官準備對你發起最後的進攻:你知道CAS這種實現方式有什麼缺點嗎?

1)一次性只能保證一個共享變數的原子性

當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享量操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。

2)迴圈會耗時

我們可以看到getAndAddInt方法執行時,如果CAS失敗,會一直進行嘗試。如果CAS長時間一直不成功,可能會給CPU帶來很大的開銷。

在併發衝突機率大的高競爭環境下,如果CAS一直失敗,會一直重試,CPU開銷較大。針對這個問題的一個思路是引入退出機制,如重試次數超過一定閾值後失敗退出。當然,更重要的是避免在高競爭環境下使用樂觀鎖。

3)存在ABA問題(重點)

先簡單解釋一下什麼是ABA

假設有兩個執行緒——執行緒1和執行緒2,兩個執行緒按照順序進行以下操作:

(1)執行緒1讀取記憶體中資料為A;

(2)執行緒2將該資料修改為B;

(3)執行緒2將該資料修改為A;

(4)執行緒1對資料進行CAS操作

在第(4)步中,由於記憶體中資料仍然為A,因此CAS操作成功,但實際上該資料已經被執行緒2修改過了。這就是ABA問題。

在AtomicInteger的例子中,ABA似乎沒有什麼危害。但是在某些場景下,ABA卻會帶來隱患,例如棧頂問題:一個棧的棧頂經過兩次(或多次)變化又恢復了原值,但是棧可能已發生了變化。

對於ABA問題,比較有效的方案是引入版本號,記憶體中的值每發生一次變化,版本號都+1;在進行CAS操作時,不僅比較記憶體中的值,也會比較版本號,只有當二者都沒有變化時,CAS才能執行成功。

所以JAVA中提供了AtomicStampedReference/AtomicMarkableReference來處理會發生ABA問題的場景,主要是在物件中額外再增加一個標記來標識物件是否有過變更。

問題

:如果記憶體地址V初次讀取的值是A,並且在準備賦值的時候檢查到它的值仍然為A,那我們就能說它的值沒有被其他執行緒改變過了嗎?

如果在這段期間它的值曾經被改成了B,後來又被改回為A,那CAS操作就會誤認為它從來沒有被改變過。這個漏洞稱為CAS操作的“ABA”問題。Java併發包為了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”(原子標記參考 ),它可以透過控制變數值的版本來保證CAS的正確性。因此,在使用CAS前要考慮清楚“ABA”問題是否會影響程式併發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。

五、適用場景

樂觀鎖和悲觀鎖並沒有優劣之分,它們有各自適合的場景;下面從兩個方面進行說明。

1)功能限制

與悲觀鎖相比,樂觀鎖適用的場景受到了更多的限制,無論是CAS還是版本號機制。

例如,CAS只能保證一個共享變數的原子操作,當涉及到多個變數時,CAS是無能為力的,而 synchronized則可以透過對整個程式碼塊加鎖來處理。再比如版本號機制,如果query的時候是針對表1,而update的時候是針對表2,也很難透過簡單的版本號來實現樂觀鎖。

2)競爭激烈程度

如果悲觀鎖和樂觀鎖都可以使用,那麼選擇就要考慮競爭的激烈程度:

1當競爭不激烈 (出現併發衝突的機率小)時,樂觀鎖更有優勢,因為悲觀鎖會鎖住程式碼塊或資料,其他執行緒無法同時訪問,影響併發,而且加鎖和釋放鎖都需要消耗額外的資源。

2當競爭激烈(出現併發衝突的機率大)時,悲觀鎖更有優勢,因為樂觀鎖在執行更新時頻繁失敗,需要不斷重試,浪費CPU資源。

希望對各位的面試有幫助,也希望小夥伴們多多支援廠長,留下你們的愛心和贊!

最後廠長祝大家能夠拿到心儀的Offer

,https://blog。csdn。net/zsr6135/article/details/120822804