樂觀鎖與悲觀鎖:
樂觀鎖和悲觀鎖是兩種思想,用于解決并發(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包下的原子類使用战转,靈活性受到限制搜立。