關于悲觀鎖和樂觀鎖

樂觀鎖與悲觀鎖:

樂觀鎖和悲觀鎖是兩種思想,用于解決并發(fā)場景下的數(shù)據(jù)競爭問題。

樂觀鎖:樂觀鎖在操作數(shù)據(jù)時非常樂觀,認為別人不會同時修改數(shù)據(jù)。

因此樂觀鎖不會上鎖戈泼,只是在執(zhí)行更新的時候判斷一下在此期間別人是否修改了數(shù)據(jù):如果別人修改了數(shù)據(jù)則放棄操作婿禽,否則執(zhí)行操作。

悲觀鎖:悲觀鎖在操作數(shù)據(jù)時比較悲觀大猛,認為別人會同時修改數(shù)據(jù)扭倾。

因此操作數(shù)據(jù)時直接把數(shù)據(jù)鎖住,直到操作完成后才會釋放鎖挽绩;上鎖期間其他人不能修改數(shù)據(jù)膛壹。

實現(xiàn)方式:

悲觀鎖的實現(xiàn)方式是加鎖,加鎖既可以是對代碼塊加鎖(如Java的synchronized關鍵字)唉堪,也可以是對數(shù)據(jù)加鎖(如MySQL中的排它鎖)模聋。

樂觀鎖的實現(xiàn)方式主要有兩種:CAS機制和版本號機制

代碼中悲觀鎖和樂觀鎖實現(xiàn):

悲觀鎖:synchronize

樂觀鎖:CAS機制(原子性操作,原子類)

CAS操作包括了3個操作數(shù):

需要讀寫的內存位置(V)

進行比較的預期值(A)

擬寫入的新值(B)

CAS是由CPU支持的原子操作唠亚,其原子性是在硬件層面進行保證的链方。

下面以Java中的自增操作(i++)為例,看一下悲觀鎖和CAS分別是如何保證線程安全的灶搜。

我們知道祟蚀,在Java中自增操作不是原子操作,它實際上包含三個獨立的操作:

讀取i值割卖;

加1前酿;

將新值寫回i

因此,如果并發(fā)執(zhí)行自增操作鹏溯,可能導致計算結果的不準確罢维。

在下面的代碼示例中:value1沒有進行任何線程安全方面的保護,value2使用了樂觀鎖(CAS)丙挽,value3使用了悲觀鎖(synchronized)言津。

運行程序,使用1000個線程同時對value1取试、value2和value3進行自增操作悬槽,可以發(fā)現(xiàn):value2和value3的值總是等于1000,而value1的值常常小于1000瞬浓。


publicclassTest{

//value1:線程不安全

privatestaticintvalue1?=0;

//value2:使用樂觀鎖

privatestaticAtomicInteger?value2?=newAtomicInteger(0);

//value3:使用悲觀鎖

privatestaticintvalue3?=0;

privatestaticsynchronizedvoidincreaseValue3(){

value3++;

}

publicstaticvoidmain(String[]?args)?throws?Exception{

//開啟1000個線程初婆,并執(zhí)行自增操作

for(inti?=0;?i?<1000;?++i){

newThread(newRunnable()?{

@Override

publicvoidrun()

{

try{

Thread.sleep(100);

}catch(InterruptedException?e)?{

e.printStackTrace();

}

value1++;

value2.getAndIncrement();

increaseValue3();

}

}).start();

}

//打印結果

Thread.sleep(1000);

System.out.println("線程不安全:"+?value1);

System.out.println("樂觀鎖(AtomicInteger):"+?value2);

System.out.println("悲觀鎖(synchronized):"+?value3);

}

}


首先來介紹AtomicInteger。

AtomicInteger是java.util.concurrent.atomic包提供的原子類,利用CPU提供的CAS操作來保證原子性磅叛;

除了AtomicInteger外屑咳,還有AtomicBoolean、AtomicLong弊琴、AtomicReference等眾多原子類兆龙。

下面看一下AtomicInteger的源碼,了解下它的自增操作getAndIncrement()是如何實現(xiàn)的(源碼以Java7為例敲董,Java8有所不同紫皇,但思想類似)。

publicclassAtomicIntegerextendsNumberimplementsjava.io.Serializable{

//存儲整數(shù)值腋寨,volatile保證可視性

privatevolatileintvalue;

//Unsafe用于實現(xiàn)對底層資源的訪問

privatestaticfinalUnsafe?unsafe?=?Unsafe.getUnsafe();

//valueOffset是value在內存中的偏移量

privatestaticfinallongvalueOffset;

//通過Unsafe獲得valueOffset

static{

try{

valueOffset?=?unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));

}catch(Exception?ex)?{thrownewError(ex);?}

}

publicfinalbooleancompareAndSet(intexpect,intupdate){

returnunsafe.compareAndSwapInt(this,?valueOffset,?expect,?update);

}

publicfinalintgetAndIncrement(){

for(;;)?{

intcurrent?=?get();

intnext?=?current?+1;

if(compareAndSet(current,?next))

returncurrent;

}

}

}

源碼分析說明如下:

1.getAndIncrement()實現(xiàn)的自增操作是自旋CAS操作:在循環(huán)中進行compareAndSet聪铺,如果執(zhí)行成功則退出,否則一直執(zhí)行萄窜。

2.其中compareAndSet是CAS操作的核心铃剔,它是利用Unsafe對象實現(xiàn)的。

3.Unsafe又是何許人也呢查刻?

Unsafe是用來幫助Java訪問操作系統(tǒng)底層資源的類(如可以分配內存键兜、釋放內存),通過Unsafe穗泵,Java具有了底層操作能力蝶押,可以提升運行效率;

強大的底層資源操作能力也帶來了安全隱患(類的名字Unsafe也在提醒我們這一點)火欧,因此正常情況下用戶無法使用棋电。

AtomicInteger在這里使用了Unsafe提供的CAS功能。

4.valueOffset可以理解為value在內存中的偏移量苇侵,對應了CAS三個操作數(shù)(V/A/B)中的V赶盔;偏移量的獲得也是通過Unsafe實現(xiàn)的。

5.value域的volatile修飾符:Java并發(fā)編程要保證線程安全榆浓,需要保證原子性于未、可視性和有序性;

CAS操作可以保證原子性陡鹃,而volatile可以保證可視性和一定程度的有序性烘浦;

在AtomicInteger中,volatile和CAS一起保證了線程安全性萍鲸。

數(shù)據(jù)庫操作中的悲觀鎖和樂觀鎖實現(xiàn):

悲觀鎖:for update

樂觀鎖:版本號方式

@Transactional

publicvoidupdateCoins(Integer?playerId){

//根據(jù)player_id查詢玩家信息

Player?player?=?query("select?coins,?level?from?player?where?player_id?=?{0}",?playerId);

//根據(jù)玩家當前信息及其他信息闷叉,計算新的金幣數(shù)

Long?newCoins?=?……;

//更新金幣數(shù)

update("update?player?set?coins?=?{0}?where?player_id?=?{1}",?newCoins,?playerId);

}

為了避免這個問題,悲觀鎖通過加鎖解決這個問題脊阴,代碼如下所示握侧。在查詢玩家信息時蚯瞧,使用select …… for update進行查詢;

該查詢語句會為該玩家數(shù)據(jù)加上排它鎖品擎,直到事務提交或回滾時才會釋放排它鎖埋合;

在此期間,如果其他線程試圖更新該玩家信息或者執(zhí)行select for update萄传,會被阻塞甚颂。

@Transactional

publicvoidupdateCoins(Integer?playerId){

//根據(jù)player_id查詢玩家信息(加排它鎖)

Player?player?=?queryForUpdate("select?coins,?level?from?player?where?player_id?=?{0}?for?update",?playerId);

//根據(jù)玩家當前信息及其他信息,計算新的金幣數(shù)

Long?newCoins?=?……;

//更新金幣數(shù)

update("update?player?set?coins?=?{0}?where?player_id?=?{1}",?newCoins,?playerId);

}

版本號機制則是另一種思路秀菱,它為玩家信息增加一個字段:version振诬。在初次查詢玩家信息時,同時查詢出version信息答朋;

在執(zhí)行update操作時,校驗version是否發(fā)生了變化棠笑,如果version變化梦碗,則不進行更新。

@Transactional

publicvoidupdateCoins(Integer?playerId){

//根據(jù)player_id查詢玩家信息蓖救,包含version信息

Player?player?=?query("select?coins,?level,?version?from?player?where?player_id?=?{0}",?playerId);

//根據(jù)玩家當前信息及其他信息洪规,計算新的金幣數(shù)

Long?newCoins?=?……;

//更新金幣數(shù),條件中增加對version的校驗

update("update?player?set?coins?=?{0},version=version+1?where?player_id?=?{1}?and?version?=?{2}",?newCoins,?playerId,?player.version);

}

三循捺、優(yōu)缺點和適用場景

樂觀鎖和悲觀鎖并沒有優(yōu)劣之分斩例,它們有各自適合的場景;下面從兩個方面進行說明从橘。

1念赶、功能限制

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

例如,CAS只能保證單個變量操作的原子性踩萎,當涉及到多個變量時停局,CAS是無能為力的,而synchronized則可以通過對整個代碼塊加鎖來處理香府。

再比如版本號機制董栽,如果query的時候是針對表1,而update的時候是針對表2企孩,也很難通過簡單的版本號來實現(xiàn)樂觀鎖锭碳。

2、競爭激烈程度

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

當競爭不激烈 (出現(xiàn)并發(fā)沖突的概率小)時工禾,樂觀鎖更有優(yōu)勢运提,因為悲觀鎖會鎖住代碼塊或數(shù)據(jù),其他線程無法同時訪問闻葵,影響并發(fā)民泵,而且加鎖和釋放鎖都需要消耗額外的資源。

當競爭激烈(出現(xiàn)并發(fā)沖突的概率大)時槽畔,悲觀鎖更有優(yōu)勢栈妆,因為樂觀鎖在執(zhí)行更新時頻繁失敗,需要不斷重試厢钧,浪費CPU資源鳞尔。

四、面試官追問:樂觀鎖加鎖嗎早直?

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

1.樂觀鎖本身是不加鎖的霞扬,只是在更新時判斷一下數(shù)據(jù)是否被其他線程更新了糕韧;AtomicInteger便是一個例子。

2.有時樂觀鎖可能與加鎖操作合作喻圃,例如萤彩,在前述updateCoins()的例子中,MySQL在執(zhí)行update時會加排它鎖斧拍。

但這只是樂觀鎖與加鎖操作合作的例子雀扶,不能改變“樂觀鎖本身不加鎖”這一事實。

五肆汹、面試官追問:CAS有哪些缺點愚墓?

面試到這里,面試官可能已經中意你了昂勉。

不過面試官準備對你發(fā)起最后的進攻:你知道CAS這種實現(xiàn)方式有什么缺點嗎转绷?

下面是CAS一些不那么完美的地方:

1、ABA問題

假設有兩個線程——線程1和線程2硼啤,兩個線程按照順序進行以下操作:

(1)線程1讀取內存中數(shù)據(jù)為A议经;

(2)線程2將該數(shù)據(jù)修改為B;

(3)線程2將該數(shù)據(jù)修改為A谴返;

(4)線程1對數(shù)據(jù)進行CAS操作

在第(4)步中煞肾,由于內存中數(shù)據(jù)仍然為A,因此CAS操作成功嗓袱,但實際上該數(shù)據(jù)已經被線程2修改過了籍救。這就是ABA問題。

在AtomicInteger的例子中渠抹,ABA似乎沒有什么危害蝙昙。

但是在某些場景下闪萄,ABA卻會帶來隱患,例如棧頂問題:一個棧的棧頂經過兩次(或多次)變化又恢復了原值奇颠,但是棸苋ィ可能已發(fā)生了變化。

對于ABA問題烈拒,比較有效的方案是引入版本號圆裕,內存中的值每發(fā)生一次變化,版本號都+1荆几;

在進行CAS操作時吓妆,不僅比較內存中的值,也會比較版本號吨铸,只有當二者都沒有變化時行拢,CAS才能執(zhí)行成功。

Java中的AtomicStampedReference類便是使用版本號來解決ABA問題的诞吱。

2舟奠、高競爭下的開銷問題

在并發(fā)沖突概率大的高競爭環(huán)境下,如果CAS一直失敗狐胎,會一直重試鸭栖,CPU開銷較大歌馍。

針對這個問題的一個思路是引入退出機制握巢,如重試次數(shù)超過一定閾值后失敗退出。

當然松却,更重要的是避免在高競爭環(huán)境下使用樂觀鎖暴浦。

3、功能限制

CAS的功能是比較受限的晓锻,例如CAS只能保證單個變量(或者說單個內存值)操作的原子性歌焦,這意味著:

(1)原子性不一定能保證線程安全,例如在Java中需要與volatile配合來保證線程安全砚哆;

(2)當涉及到多個變量(內存值)時独撇,CAS也無能為力。

除此之外躁锁,CAS的實現(xiàn)需要硬件層面處理器的支持纷铣,在Java中普通用戶無法直接使用,只能借助atomic包下的原子類使用战转,靈活性受到限制搜立。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市槐秧,隨后出現(xiàn)的幾起案子啄踊,更是在濱河造成了極大的恐慌忧设,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颠通,死亡現(xiàn)場離奇詭異址晕,居然都是意外死亡,警方通過查閱死者的電腦和手機蒜哀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門斩箫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人撵儿,你說我怎么就攤上這事乘客。” “怎么了淀歇?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵易核,是天一觀的道長。 經常有香客問我浪默,道長牡直,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任纳决,我火速辦了婚禮碰逸,結果婚禮上,老公的妹妹穿的比我還像新娘阔加。我一直安慰自己饵史,他們只是感情好,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布胜榔。 她就那樣靜靜地躺著胳喷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪夭织。 梳的紋絲不亂的頭發(fā)上吭露,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音尊惰,去河邊找鬼讲竿。 笑死,一個胖子當著我的面吹牛弄屡,可吹牛的內容都是我干的题禀。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼琢岩,長吁一口氣:“原來是場噩夢啊……” “哼投剥!你這毒婦竟也來了?” 一聲冷哼從身側響起担孔,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤江锨,失蹤者是張志新(化名)和其女友劉穎吃警,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體啄育,經...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡酌心,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了挑豌。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片安券。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖氓英,靈堂內的尸體忽然破棺而出侯勉,到底是詐尸還是另有隱情,我是刑警寧澤铝阐,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布址貌,位于F島的核電站,受9級特大地震影響徘键,放射性物質發(fā)生泄漏练对。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一吹害、第九天 我趴在偏房一處隱蔽的房頂上張望螟凭。 院中可真熱鬧,春花似錦它呀、人聲如沸螺男。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烟号。三九已至绊谭,卻和暖如春政恍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背达传。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工篙耗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人宪赶。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓宗弯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親搂妻。 傳聞我的和親對象是個殘疾皇子蒙保,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內容

  • 前言 樂觀鎖和悲觀鎖問題邓厕,是出現(xiàn)頻率比較高的面試題逝嚎。本文將由淺入深,逐步介紹它們的基本概念详恼、實現(xiàn)方式(含實例)补君、適...
    程序員BUG閱讀 3,239評論 3 89
  • 前言 樂觀鎖和悲觀鎖問題挽铁,是出現(xiàn)頻率比較高的面試題。本文將由淺入深敞掘,逐步介紹它們的基本概念叽掘、實現(xiàn)方式(含實例)、適...
    java成功之路閱讀 586評論 0 3
  • 目錄 基本概念 實現(xiàn)方式(含實例) 優(yōu)缺點和適用場景 面試官追問:樂觀鎖加鎖嗎玖雁? 面試官追問:CAS有哪些缺點够掠? ...
    未名枯草閱讀 653評論 0 2
  • 討論了一個多小時的哲學問題 最后發(fā)現(xiàn)毫無意義 人間到底值不值得 浪費時間的事情 現(xiàn)在只保留了最后兩件 一件和周圍人...
    夏天的秘密花園閱讀 46評論 0 0
  • 今日統(tǒng)計 1.高數(shù),2小時02 2.英語閱讀茄菊,1小時09 3.《C++程序設計》疯潭,3小時52 洗漱 0818--1...
    天熱開風扇閱讀 14評論 0 1