一、樂(lè)觀鎖 & 悲觀鎖
1.1 樂(lè)觀鎖的定義
樂(lè)觀鎖,顧名思義,他比較樂(lè)觀,他認(rèn)為一般情況下不會(huì)出現(xiàn)沖突,所以只會(huì)在更新數(shù)據(jù)的時(shí)候才會(huì)對(duì)沖突進(jìn)行檢測(cè)。如果沒(méi)有發(fā)生沖突直接進(jìn)行修改,如果發(fā)生了沖突則不進(jìn)行任何修改,然后把結(jié)果返回給用戶,讓用戶自行處理。
1.1.1樂(lè)觀鎖的實(shí)現(xiàn)-CAS
樂(lè)觀鎖的實(shí)現(xiàn)并不是給數(shù)據(jù)加鎖 ,而是通過(guò)CAS(Compare And Swap)比較并替換,來(lái)實(shí)現(xiàn)樂(lè)觀鎖的效果。
CAS比較并替換的流程是這樣子的:CAS中包含了三個(gè)操作,單位:V(內(nèi)存值)、A(預(yù)期的舊址)、B(新值),比較V值和A值是否相等,,如果相等的話則將V的值更換成B,否則就提示用戶修改失敗,從而實(shí)現(xiàn)了CAS機(jī)制。
這只是定義的流程,但是在實(shí)際執(zhí)行過(guò)程中,并不會(huì)當(dāng)V值和A值不相等時(shí),就立即把結(jié)果返回給用戶,而是將A(預(yù)期的舊值)改為內(nèi)存中最新的值,然后再進(jìn)行比較,直到V值也A值相等,修改內(nèi)存中的值為B結(jié)束。
可能你還是覺(jué)得有些晦澀,那我們舉個(gè)栗子:
看完這個(gè)圖相信你一定能理解了CAS的執(zhí)行流程了。
1.1.2 CAS的應(yīng)用
CAS的底層實(shí)現(xiàn)是靠Unsafe類實(shí)現(xiàn)的,Unsafe是CAS的核心類,由于Java方法無(wú)法直接訪問(wèn)底層系統(tǒng),需要通過(guò)本地(Native)方法來(lái)訪問(wèn),Unsafe相當(dāng)于一個(gè)后門(mén),基于該類可以直接操作特定的內(nèi)存數(shù)據(jù)。Unsafe類存在sun.misc包中,其內(nèi)部方法操作可以像C的指針一樣直接操作內(nèi)存,因?yàn)镴ava中的CAS操作的執(zhí)行依賴于Unsafe類的方法。
注意Unsafe類的所有方法都是native修飾的,也就是說(shuō)Unsafe類中的方法都直接調(diào)用操作系統(tǒng)底層資源執(zhí)行相應(yīng)的任務(wù)。因此不推薦使用Unsafe類,如果用不好會(huì)對(duì)底層資源造成影響。
為什么Atomic修飾的包裝類,能夠保證原子性,依靠的就是底層的unsafe類,我們來(lái)看看AtomicInteger的源碼:
在getAndIncrement方法中還調(diào)用了unsafe的方法,因此這也就是為什么它能夠保證原子性的原因。
因此我們可以利用Atomic+包裝類實(shí)現(xiàn)線程安全的問(wèn)題。
importjava.util.concurrent.atomic.AtomicInteger; /** *使用AtomicInteger保證線程安全問(wèn)題 */ publicclassAtomicIntegerDemo{ staticclassCounter{ privatestaticAtomicIntegernum=newAtomicInteger(0); privateintMAX_COUNT=100000; publicCounter(intMAX_COUNT){ this.MAX_COUNT=MAX_COUNT; } //++方法 publicvoidincrement(){ for(inti=0;i{ counter.increment(); }); Threadthread2=newThread(()->{ counter.decrement(); }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("最終結(jié)果:"+counter.getNum()); } }

1.1.3 CAS存在的問(wèn)題
循環(huán)時(shí)間長(zhǎng),開(kāi)銷大
只能保證一個(gè)共享變量的原子性操作(可以通過(guò)循環(huán)CAS的方式實(shí)現(xiàn))
存在ABA問(wèn)題
1.1.4 ABA問(wèn)題
什么時(shí)ABA問(wèn)題呢?
比如說(shuō)兩個(gè)線程t1和t2,t1的執(zhí)行時(shí)間為10s,t2的執(zhí)行時(shí)間為2s,剛開(kāi)始都從主內(nèi)存中獲取到A值,t2先開(kāi)始執(zhí)行,他執(zhí)行的比較快,于是他將A的值先改為B,再改為A,這時(shí)t1執(zhí)行,判斷內(nèi)存中的值為A,與自己預(yù)期的值一樣,以為這個(gè)值沒(méi)有修改過(guò),于是將內(nèi)存中的值修改為B,但是實(shí)際上中間可能已經(jīng)經(jīng)歷了許多:A->B->A。
所以ABA問(wèn)題就是,在我們進(jìn)行CAS中的比較時(shí),預(yù)期的值與內(nèi)存中的值一樣,并不能說(shuō)明這個(gè)值沒(méi)有被改過(guò),而是可能已經(jīng)被修改了,但是又被改回了預(yù)期的值。
importjava.util.concurrent.atomic.AtomicInteger; /** *ABA問(wèn)題演示 */ publicclassABADemo1{ privatestaticAtomicIntegermoney=newAtomicInteger(100); publicstaticvoidmain(String[]args)throwsInterruptedException{ //第一次點(diǎn)轉(zhuǎn)賬按鈕(-50) Threadt1=newThread(()->{ intold_money=money.get();//先得到余額 try{//執(zhí)行花費(fèi)2s Thread.sleep(2000); }catch(InterruptedExceptione){ e.printStackTrace(); } money.compareAndSet(old_money,old_money-50); }); t1.start(); //第二次點(diǎn)擊轉(zhuǎn)賬按鈕(-50)不小心點(diǎn)擊的,因?yàn)榈谝淮吸c(diǎn)擊之后沒(méi)反應(yīng),所以不小心又點(diǎn)了一次 Threadt2=newThread(()->{ intold_money=money.get();//先得到余額 money.compareAndSet(old_money,old_money-50); }); t2.start(); //給賬戶加50 Threadt3=newThread(()->{ //執(zhí)行花費(fèi)1s try{ Thread.sleep(1000); }catch(InterruptedExceptione){ e.printStackTrace(); } intold_money=money.get(); money.compareAndSet(old_money,old_money+50); }); t3.start(); t1.join(); t2.join(); t3.join(); System.out.println("最終的錢(qián)數(shù):"+money.get()); } }

這個(gè)例子演示了ABA問(wèn)題,A有100元,A向B轉(zhuǎn)錢(qián),第一次轉(zhuǎn)了50元,但是點(diǎn)完轉(zhuǎn)賬按鈕沒(méi)有反應(yīng),于是又點(diǎn)擊了一次。第一次轉(zhuǎn)賬成功后A還剩50元,而這時(shí)C給A轉(zhuǎn)了50元,A的余額變?yōu)?00元,第二次的CAS判斷(100,100,50),A的余額與預(yù)期的值一樣,于是將A的余額修改為50元。
1.1.5 ABA問(wèn)題的解決方案
由于CAS是只管頭和尾是否相等,若相等,就認(rèn)為這個(gè)過(guò)程沒(méi)問(wèn)題,因此我們就引出了AtomicStampedReference,時(shí)間戳原子引用,在這里應(yīng)用于版本號(hào)的更新。也就是我們新增了一種機(jī)制,在每次更新的時(shí)候,需要比較當(dāng)前值和期望值以及當(dāng)前版本號(hào)和期望版本號(hào),若值或版本號(hào)有一個(gè)不相同,這個(gè)過(guò)程都是有問(wèn)題的。
我們來(lái)看上面的例子怎么用AtomicStampedReference解決呢?
importjava.util.concurrent.atomic.AtomicInteger; importjava.util.concurrent.atomic.AtomicStampedReference; /** *ABA問(wèn)題解決添加版本號(hào) */ publicclassABADemo2{ privatestaticAtomicStampedReferencemoney= newAtomicStampedReference<>(100,0); publicstaticvoidmain(String[]args)throwsInterruptedException{ //第一次點(diǎn)轉(zhuǎn)賬按鈕(-50) Threadt1=newThread(()->{ intold_money=money.getReference();//先得到余額100 intoldStamp=money.getStamp();//得到舊的版本號(hào) try{//執(zhí)行花費(fèi)2s Thread.sleep(2000); }catch(InterruptedExceptione){ e.printStackTrace(); } booleanresult=money.compareAndSet(old_money,old_money-50,oldStamp,oldStamp+1); System.out.println(Thread.currentThread().getName()+"轉(zhuǎn)賬:"+result); },"線程1"); t1.start(); //第二次點(diǎn)擊轉(zhuǎn)賬按鈕(-50)不小心點(diǎn)擊的,因?yàn)榈谝淮吸c(diǎn)擊之后沒(méi)反應(yīng),所以不小心又點(diǎn)了一次 Threadt2=newThread(()->{ intold_money=money.getReference();//先得到余額100 intoldStamp=money.getStamp();//得到舊的版本號(hào) booleanresult=money.compareAndSet(old_money,old_money-50,oldStamp,oldStamp+1); System.out.println(Thread.currentThread().getName()+"轉(zhuǎn)賬:"+result); },"線程2"); t2.start(); //給賬戶+50 Threadt3=newThread(()->{ //執(zhí)行花費(fèi)1s try{ Thread.sleep(1000); }catch(InterruptedExceptione){ e.printStackTrace(); } intold_money=money.getReference();//先得到余額100 intoldStamp=money.getStamp();//得到舊的版本號(hào) booleanresult=money.compareAndSet(old_money,old_money+50,oldStamp,oldStamp+1); System.out.println(Thread.currentThread().getName()+"發(fā)工資:"+result); },"線程3"); t3.start(); t1.join(); t2.join(); t3.join(); System.out.println("最終的錢(qián)數(shù):"+money.getReference()); } }

AtommicStampedReference解決了ABA問(wèn)題,在每次更新值之前,比較值和版本號(hào)。
1.2 悲觀鎖
什么是悲觀鎖?
悲觀鎖就是比較悲觀,總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時(shí)候都會(huì)認(rèn)為別人會(huì)修改,所以在每次拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿數(shù)據(jù)就會(huì)阻塞直到它拿到鎖。
比如我們之前提到的synchronized和Lock都是悲觀鎖。
二、公平鎖和非公平鎖
公平鎖: 按照線程來(lái)的先后順序獲取鎖,當(dāng)一個(gè)線程釋放鎖之后,那么就喚醒阻塞隊(duì)列中第一個(gè)線程獲取鎖。
非公平鎖: 不是按照線程來(lái)的先后順序喚醒鎖,而是當(dāng)有一個(gè)線程釋放鎖之后,喚醒阻塞隊(duì)列中的所有線程,隨機(jī)獲取鎖。
之前在講synchronized和Lock這兩個(gè)鎖解決線程安全問(wèn)題線程安全問(wèn)題的解決的時(shí)候,我們提過(guò):
synchronized的鎖只能是非公平鎖;
Lock的鎖默認(rèn)情況下是非公平鎖,而擋在構(gòu)造 函數(shù)中傳入?yún)?shù)時(shí),則是公平鎖;
公平鎖:Lock lock=new ReentrantLock(true);
非公平鎖:Lock lock=new ReentrantLock();
由于公平鎖只能按照線程來(lái)的線程順序獲取鎖,因此性能較低,推薦使用非公平鎖。
三、讀寫(xiě)鎖
3.1 讀寫(xiě)鎖
讀寫(xiě)鎖顧名思義是一把鎖分為兩部分:讀鎖和寫(xiě)鎖。
讀寫(xiě)鎖的規(guī)則是:允許多個(gè)線程獲取讀鎖,而寫(xiě)鎖是互斥鎖,不允許多個(gè)線程同時(shí)獲得,并且讀操作和寫(xiě)操作也是 互斥的,總的來(lái)說(shuō)就是讀讀不互斥,讀寫(xiě)互斥,寫(xiě)寫(xiě)互斥。
為什么要這樣設(shè)置呢?
讓整個(gè)讀寫(xiě)的操作到設(shè)置為互斥不是更方便嗎?
其實(shí)只要涉及到“互斥”,就會(huì)產(chǎn)生線程掛起等待,一旦掛起等待,,再次被喚醒就不知道什么時(shí)候了,因此盡可能的減少“互斥"的機(jī)會(huì),就是提高效率的重要途徑。
Java標(biāo)準(zhǔn)庫(kù)提供了ReentrantReadWriteLock類實(shí)現(xiàn)了讀寫(xiě)鎖。
ReentrantReadWriteLock.ReadLock類表示一個(gè)讀鎖,提供了lock和unlock進(jìn)行加鎖和解鎖。
ReentrantReadWriteLock.WriteLock類表示一個(gè)寫(xiě)鎖,提供了lock和unlock進(jìn)行加鎖和解鎖。
下面我們來(lái)看下讀寫(xiě)鎖的使用演示~
importjava.time.LocalDateTime; importjava.util.concurrent.LinkedBlockingDeque; importjava.util.concurrent.ThreadPoolExecutor; importjava.util.concurrent.TimeUnit; importjava.util.concurrent.locks.ReentrantReadWriteLock; /** *演示讀寫(xiě)鎖的使用 */ publicclassReadWriteLockDemo1{ publicstaticvoidmain(String[]args){ //創(chuàng)建讀寫(xiě)鎖 finalReentrantReadWriteLockreentrantReadWriteLock=newReentrantReadWriteLock(); //創(chuàng)建讀鎖 finalReentrantReadWriteLock.ReadLockreadLock=reentrantReadWriteLock.readLock(); //創(chuàng)建寫(xiě)鎖 finalReentrantReadWriteLock.WriteLockwriteLock=reentrantReadWriteLock.writeLock(); //線程池 ThreadPoolExecutorexecutor=newThreadPoolExecutor(5,5,0,TimeUnit.SECONDS,newLinkedBlockingDeque<>(100)); //啟動(dòng)線程執(zhí)行任務(wù)【讀操作1】 executor.submit(()->{ //加鎖操作 readLock.lock(); try{ //執(zhí)行業(yè)務(wù)邏輯 System.out.println("執(zhí)行讀鎖1:"+LocalDateTime.now()); TimeUnit.SECONDS.sleep(1); }catch(InterruptedExceptione){ e.printStackTrace(); }finally{ readLock.unlock(); } }); //啟動(dòng)線程執(zhí)行任務(wù)【讀操作2】 executor.submit(()->{ //加鎖操作 readLock.lock(); try{ //執(zhí)行業(yè)務(wù)邏輯 System.out.println("執(zhí)行讀鎖2:"+LocalDateTime.now()); TimeUnit.SECONDS.sleep(1); }catch(InterruptedExceptione){ e.printStackTrace(); }finally{ //釋放鎖 readLock.unlock(); } }); //啟動(dòng)線程執(zhí)行【寫(xiě)操作1】 executor.submit(()->{ //加鎖 writeLock.lock(); try{ System.out.println("執(zhí)行寫(xiě)鎖1:"+LocalDateTime.now()); TimeUnit.SECONDS.sleep(1); }catch(InterruptedExceptione){ e.printStackTrace(); }finally{ writeLock.unlock(); } }); //啟動(dòng)線程執(zhí)行【寫(xiě)操作2】 executor.submit(()->{ //加鎖 writeLock.lock(); try{ System.out.println("執(zhí)行寫(xiě)鎖2:"+LocalDateTime.now()); TimeUnit.SECONDS.sleep(1); }catch(InterruptedExceptione){ e.printStackTrace(); }finally{ writeLock.unlock(); } }); } }

根據(jù)運(yùn)行結(jié)果我們看到,讀鎖操作是一起執(zhí)行的,而寫(xiě)鎖操作是互斥執(zhí)行的。
3.2 獨(dú)占鎖
獨(dú)占鎖就是指任何時(shí)候只能有一個(gè)線程能執(zhí)行資源操作,是互斥的。
比如寫(xiě)鎖,就是一個(gè)獨(dú)占鎖,任何時(shí)候只能有一個(gè)線程執(zhí)行寫(xiě)操作,synchronized、Lock都是獨(dú)占鎖。
3.3 共享鎖
共享鎖是指可以同時(shí)被多個(gè)線程獲取,但是只能被一個(gè)線程修改。讀寫(xiě)鎖就是一個(gè)典型的共享鎖,它允許多個(gè)線程進(jìn)行讀操作 ,但是只允許一個(gè)線程進(jìn)行寫(xiě)操作。
四、可重入鎖 & 自旋鎖
4.1 可重入鎖
可重入鎖指的是該線程獲取了該鎖之后,可以無(wú)限次的進(jìn)入該鎖。
因?yàn)樵趯?duì)象頭存儲(chǔ)了擁有當(dāng)前鎖的id,進(jìn)入鎖之前驗(yàn)證對(duì)象頭的id是否與當(dāng)前線程id一致,若一致就可進(jìn)入,因此實(shí)現(xiàn)可重入鎖 。
4.2 自旋鎖
自旋鎖是指嘗試獲取鎖的線程不會(huì)立即阻塞,而是采取循環(huán)的方式嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗。線程上下文切換就是從用戶態(tài)—>內(nèi)核態(tài)。
synchronized就是一種自適應(yīng)自旋鎖(自旋的次數(shù)不固定),hotSpot虛擬機(jī)的自旋機(jī)制是這一次的自旋次數(shù)由上一次自旋獲取鎖的次數(shù)來(lái)決定,如果上次自旋了很多次才獲取到鎖,那么這次自旋的次數(shù)就會(huì)降低,因?yàn)樘摂M機(jī)認(rèn)為這一次大概率還是要自旋很多次才能獲取到鎖,比較浪費(fèi)系統(tǒng)資源。
審核編輯:劉清
-
虛擬機(jī)
+關(guān)注
關(guān)注
1文章
955瀏覽量
28886 -
CAS
+關(guān)注
關(guān)注
0文章
35瀏覽量
15329 -
ABAT
+關(guān)注
關(guān)注
0文章
2瀏覽量
6333
原文標(biāo)題:一篇文章搞定,多線程常見(jiàn)鎖策略+CAS
文章出處:【微信號(hào):AndroidPush,微信公眾號(hào):Android編程精選】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
Java多線程的用法
C++面向?qū)ο?b class='flag-5'>多線程編程 (pdf電子版)
QNX環(huán)境下多線程編程
多線程技術(shù)在串口通信中的應(yīng)用
多線程細(xì)節(jié)問(wèn)題學(xué)習(xí)筆記

多線程好還是單線程好?單線程和多線程的區(qū)別 優(yōu)缺點(diǎn)分析
mfc多線程編程實(shí)例及代碼,mfc多線程間通信介紹

什么是多線程編程?多線程編程基礎(chǔ)知識(shí)
java學(xué)習(xí)——java面試【事務(wù)、鎖、多線程】資料整理
多線程編程指南的PDF電子書(shū)免費(fèi)下載

無(wú)鎖CAS如何實(shí)現(xiàn)各種無(wú)鎖的數(shù)據(jù)結(jié)構(gòu)

評(píng)論