一、樂觀鎖 & 悲觀鎖
1.1 樂觀鎖的定義
樂觀鎖,顧名思義,他比較樂觀,他認為一般情況下不會出現沖突,所以只會在更新數據的時候才會對沖突進行檢測。如果沒有發生沖突直接進行修改,如果發生了沖突則不進行任何修改,然后把結果返回給用戶,讓用戶自行處理。
1.1.1樂觀鎖的實現-CAS
樂觀鎖的實現并不是給數據加鎖 ,而是通過CAS(Compare And Swap)比較并替換,來實現樂觀鎖的效果。
CAS比較并替換的流程是這樣子的:CAS中包含了三個操作,單位:V(內存值)、A(預期的舊址)、B(新值),比較V值和A值是否相等,,如果相等的話則將V的值更換成B,否則就提示用戶修改失敗,從而實現了CAS機制。
這只是定義的流程,但是在實際執行過程中,并不會當V值和A值不相等時,就立即把結果返回給用戶,而是將A(預期的舊值)改為內存中最新的值,然后再進行比較,直到V值也A值相等,修改內存中的值為B結束。
可能你還是覺得有些晦澀,那我們舉個栗子:
看完這個圖相信你一定能理解了CAS的執行流程了。
1.1.2 CAS的應用
CAS的底層實現是靠Unsafe類實現的,Unsafe是CAS的核心類,由于Java方法無法直接訪問底層系統,需要通過本地(Native)方法來訪問,Unsafe相當于一個后門,基于該類可以直接操作特定的內存數據。Unsafe類存在sun.misc包中,其內部方法操作可以像C的指針一樣直接操作內存,因為Java中的CAS操作的執行依賴于Unsafe類的方法。
注意Unsafe類的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接調用操作系統底層資源執行相應的任務。因此不推薦使用Unsafe類,如果用不好會對底層資源造成影響。
為什么Atomic修飾的包裝類,能夠保證原子性,依靠的就是底層的unsafe類,我們來看看AtomicInteger的源碼:
在getAndIncrement方法中還調用了unsafe的方法,因此這也就是為什么它能夠保證原子性的原因。
因此我們可以利用Atomic+包裝類實現線程安全的問題。
importjava.util.concurrent.atomic.AtomicInteger; /** *使用AtomicInteger保證線程安全問題 */ 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("最終結果:"+counter.getNum()); } }
1.1.3 CAS存在的問題
循環時間長,開銷大
只能保證一個共享變量的原子性操作(可以通過循環CAS的方式實現)
存在ABA問題
1.1.4 ABA問題
什么時ABA問題呢?
比如說兩個線程t1和t2,t1的執行時間為10s,t2的執行時間為2s,剛開始都從主內存中獲取到A值,t2先開始執行,他執行的比較快,于是他將A的值先改為B,再改為A,這時t1執行,判斷內存中的值為A,與自己預期的值一樣,以為這個值沒有修改過,于是將內存中的值修改為B,但是實際上中間可能已經經歷了許多:A->B->A。
所以ABA問題就是,在我們進行CAS中的比較時,預期的值與內存中的值一樣,并不能說明這個值沒有被改過,而是可能已經被修改了,但是又被改回了預期的值。
importjava.util.concurrent.atomic.AtomicInteger; /** *ABA問題演示 */ publicclassABADemo1{ privatestaticAtomicIntegermoney=newAtomicInteger(100); publicstaticvoidmain(String[]args)throwsInterruptedException{ //第一次點轉賬按鈕(-50) Threadt1=newThread(()->{ intold_money=money.get();//先得到余額 try{//執行花費2s Thread.sleep(2000); }catch(InterruptedExceptione){ e.printStackTrace(); } money.compareAndSet(old_money,old_money-50); }); t1.start(); //第二次點擊轉賬按鈕(-50)不小心點擊的,因為第一次點擊之后沒反應,所以不小心又點了一次 Threadt2=newThread(()->{ intold_money=money.get();//先得到余額 money.compareAndSet(old_money,old_money-50); }); t2.start(); //給賬戶加50 Threadt3=newThread(()->{ //執行花費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("最終的錢數:"+money.get()); } }
這個例子演示了ABA問題,A有100元,A向B轉錢,第一次轉了50元,但是點完轉賬按鈕沒有反應,于是又點擊了一次。第一次轉賬成功后A還剩50元,而這時C給A轉了50元,A的余額變為100元,第二次的CAS判斷(100,100,50),A的余額與預期的值一樣,于是將A的余額修改為50元。
1.1.5 ABA問題的解決方案
由于CAS是只管頭和尾是否相等,若相等,就認為這個過程沒問題,因此我們就引出了AtomicStampedReference,時間戳原子引用,在這里應用于版本號的更新。也就是我們新增了一種機制,在每次更新的時候,需要比較當前值和期望值以及當前版本號和期望版本號,若值或版本號有一個不相同,這個過程都是有問題的。
我們來看上面的例子怎么用AtomicStampedReference解決呢?
importjava.util.concurrent.atomic.AtomicInteger; importjava.util.concurrent.atomic.AtomicStampedReference; /** *ABA問題解決添加版本號 */ publicclassABADemo2{ privatestaticAtomicStampedReferencemoney= newAtomicStampedReference<>(100,0); publicstaticvoidmain(String[]args)throwsInterruptedException{ //第一次點轉賬按鈕(-50) Threadt1=newThread(()->{ intold_money=money.getReference();//先得到余額100 intoldStamp=money.getStamp();//得到舊的版本號 try{//執行花費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()+"轉賬:"+result); },"線程1"); t1.start(); //第二次點擊轉賬按鈕(-50)不小心點擊的,因為第一次點擊之后沒反應,所以不小心又點了一次 Threadt2=newThread(()->{ intold_money=money.getReference();//先得到余額100 intoldStamp=money.getStamp();//得到舊的版本號 booleanresult=money.compareAndSet(old_money,old_money-50,oldStamp,oldStamp+1); System.out.println(Thread.currentThread().getName()+"轉賬:"+result); },"線程2"); t2.start(); //給賬戶+50 Threadt3=newThread(()->{ //執行花費1s try{ Thread.sleep(1000); }catch(InterruptedExceptione){ e.printStackTrace(); } intold_money=money.getReference();//先得到余額100 intoldStamp=money.getStamp();//得到舊的版本號 booleanresult=money.compareAndSet(old_money,old_money+50,oldStamp,oldStamp+1); System.out.println(Thread.currentThread().getName()+"發工資:"+result); },"線程3"); t3.start(); t1.join(); t2.join(); t3.join(); System.out.println("最終的錢數:"+money.getReference()); } }
AtommicStampedReference解決了ABA問題,在每次更新值之前,比較值和版本號。
1.2 悲觀鎖
什么是悲觀鎖?
悲觀鎖就是比較悲觀,總是假設最壞的情況,每次去拿數據的時候都會認為別人會修改,所以在每次拿數據的時候都會上鎖,這樣別人想拿數據就會阻塞直到它拿到鎖。
比如我們之前提到的synchronized和Lock都是悲觀鎖。
二、公平鎖和非公平鎖
公平鎖: 按照線程來的先后順序獲取鎖,當一個線程釋放鎖之后,那么就喚醒阻塞隊列中第一個線程獲取鎖。
非公平鎖: 不是按照線程來的先后順序喚醒鎖,而是當有一個線程釋放鎖之后,喚醒阻塞隊列中的所有線程,隨機獲取鎖。
之前在講synchronized和Lock這兩個鎖解決線程安全問題線程安全問題的解決的時候,我們提過:
synchronized的鎖只能是非公平鎖;
Lock的鎖默認情況下是非公平鎖,而擋在構造 函數中傳入參數時,則是公平鎖;
公平鎖:Lock lock=new ReentrantLock(true);
非公平鎖:Lock lock=new ReentrantLock();
由于公平鎖只能按照線程來的線程順序獲取鎖,因此性能較低,推薦使用非公平鎖。
三、讀寫鎖
3.1 讀寫鎖
讀寫鎖顧名思義是一把鎖分為兩部分:讀鎖和寫鎖。
讀寫鎖的規則是:允許多個線程獲取讀鎖,而寫鎖是互斥鎖,不允許多個線程同時獲得,并且讀操作和寫操作也是 互斥的,總的來說就是讀讀不互斥,讀寫互斥,寫寫互斥。
為什么要這樣設置呢?
讓整個讀寫的操作到設置為互斥不是更方便嗎?
其實只要涉及到“互斥”,就會產生線程掛起等待,一旦掛起等待,,再次被喚醒就不知道什么時候了,因此盡可能的減少“互斥"的機會,就是提高效率的重要途徑。
Java標準庫提供了ReentrantReadWriteLock類實現了讀寫鎖。
ReentrantReadWriteLock.ReadLock類表示一個讀鎖,提供了lock和unlock進行加鎖和解鎖。
ReentrantReadWriteLock.WriteLock類表示一個寫鎖,提供了lock和unlock進行加鎖和解鎖。
下面我們來看下讀寫鎖的使用演示~
importjava.time.LocalDateTime; importjava.util.concurrent.LinkedBlockingDeque; importjava.util.concurrent.ThreadPoolExecutor; importjava.util.concurrent.TimeUnit; importjava.util.concurrent.locks.ReentrantReadWriteLock; /** *演示讀寫鎖的使用 */ publicclassReadWriteLockDemo1{ publicstaticvoidmain(String[]args){ //創建讀寫鎖 finalReentrantReadWriteLockreentrantReadWriteLock=newReentrantReadWriteLock(); //創建讀鎖 finalReentrantReadWriteLock.ReadLockreadLock=reentrantReadWriteLock.readLock(); //創建寫鎖 finalReentrantReadWriteLock.WriteLockwriteLock=reentrantReadWriteLock.writeLock(); //線程池 ThreadPoolExecutorexecutor=newThreadPoolExecutor(5,5,0,TimeUnit.SECONDS,newLinkedBlockingDeque<>(100)); //啟動線程執行任務【讀操作1】 executor.submit(()->{ //加鎖操作 readLock.lock(); try{ //執行業務邏輯 System.out.println("執行讀鎖1:"+LocalDateTime.now()); TimeUnit.SECONDS.sleep(1); }catch(InterruptedExceptione){ e.printStackTrace(); }finally{ readLock.unlock(); } }); //啟動線程執行任務【讀操作2】 executor.submit(()->{ //加鎖操作 readLock.lock(); try{ //執行業務邏輯 System.out.println("執行讀鎖2:"+LocalDateTime.now()); TimeUnit.SECONDS.sleep(1); }catch(InterruptedExceptione){ e.printStackTrace(); }finally{ //釋放鎖 readLock.unlock(); } }); //啟動線程執行【寫操作1】 executor.submit(()->{ //加鎖 writeLock.lock(); try{ System.out.println("執行寫鎖1:"+LocalDateTime.now()); TimeUnit.SECONDS.sleep(1); }catch(InterruptedExceptione){ e.printStackTrace(); }finally{ writeLock.unlock(); } }); //啟動線程執行【寫操作2】 executor.submit(()->{ //加鎖 writeLock.lock(); try{ System.out.println("執行寫鎖2:"+LocalDateTime.now()); TimeUnit.SECONDS.sleep(1); }catch(InterruptedExceptione){ e.printStackTrace(); }finally{ writeLock.unlock(); } }); } }
根據運行結果我們看到,讀鎖操作是一起執行的,而寫鎖操作是互斥執行的。
3.2 獨占鎖
獨占鎖就是指任何時候只能有一個線程能執行資源操作,是互斥的。
比如寫鎖,就是一個獨占鎖,任何時候只能有一個線程執行寫操作,synchronized、Lock都是獨占鎖。
3.3 共享鎖
共享鎖是指可以同時被多個線程獲取,但是只能被一個線程修改。讀寫鎖就是一個典型的共享鎖,它允許多個線程進行讀操作 ,但是只允許一個線程進行寫操作。
四、可重入鎖 & 自旋鎖
4.1 可重入鎖
可重入鎖指的是該線程獲取了該鎖之后,可以無限次的進入該鎖。
因為在對象頭存儲了擁有當前鎖的id,進入鎖之前驗證對象頭的id是否與當前線程id一致,若一致就可進入,因此實現可重入鎖 。
4.2 自旋鎖
自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采取循環的方式嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗。線程上下文切換就是從用戶態—>內核態。
synchronized就是一種自適應自旋鎖(自旋的次數不固定),hotSpot虛擬機的自旋機制是這一次的自旋次數由上一次自旋獲取鎖的次數來決定,如果上次自旋了很多次才獲取到鎖,那么這次自旋的次數就會降低,因為虛擬機認為這一次大概率還是要自旋很多次才能獲取到鎖,比較浪費系統資源。
審核編輯:劉清
-
虛擬機
+關注
關注
1文章
919瀏覽量
28279 -
CAS
+關注
關注
0文章
35瀏覽量
15213 -
ABAT
+關注
關注
0文章
2瀏覽量
6286
原文標題:一篇文章搞定,多線程常見鎖策略+CAS
文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論