文章目錄
- 悲觀/樂觀鎖
- 掛起等待鎖/自旋鎖
- 偏向鎖
- 輕量級/重量級鎖
- 鎖升級
- CAS
- CAS引發的ABA問題
- 解決方案
- 原子類
- 公平/不公平鎖
- 可重入鎖
- ReentrantLock
- 讀寫鎖
- Callable接口
這里的“悲觀”“樂觀”“掛起等待”“自旋”“輕量級”“重量級”“公平”“非公平”“可重入”僅代表某個鎖的特性,不具體指某個鎖。
悲觀/樂觀鎖
悲觀鎖:總是在考慮最壞的情況,認為在拿數據時總是存在線程不安全問題,總是認為別人在同時修改這個數據,于是每次拿的時候給這個數據上鎖,讓其他線程拿不了直到鎖釋放。代表有synchronized、ReentrantLock。(重量級鎖有悲觀鎖的影子!)
樂觀:總是考慮最好的情況,認為總是不存在有線程在同時修改的情況,放心的去拿數據,但是樂觀鎖有CAS的機制,如果要進行更新操作,發現讀取到更新之間有其他線程修改了這個數據,則會放棄修改,重新讀取再修改直到成功。代表有原子類,原子類底層就是不斷的CAS。(輕量級鎖、偏向鎖都有樂觀鎖的影子!)
掛起等待鎖/自旋鎖
自旋鎖:線程未獲得鎖則不斷嘗試獲取鎖。可以及時獲得鎖,CPU空轉(忙等),但缺點是可能會很消耗CPU資源,因此自旋到一定條件(設定時間、次數)就不自旋了。
while(搶鎖失敗){ 搶;if(!自旋條件) break;
}
掛起等待鎖:線程未獲得鎖則進入阻塞狀態,等待被喚醒。不會很消耗CPU資源,但缺點是等待周期可能會很長,不能及時獲得鎖。
偏向鎖
當同步代碼塊只有一個線程運行時(可以理解為只有一個線程參與鎖競爭),此時并不會給這個線程加鎖,而是給這個線程一個標記ID(由JVM記錄,這個機制也實現了Java的可重入鎖,方便理解可參考我寫的解決死鎖段落),當下一次進入同步代碼塊時,根據這個ID判斷是不是仍然還是這個線程,如果是則直接運行,否則偏向鎖升級為輕量級鎖,鎖持有者為獲取鎖的線程,其他線程自旋。
上面的過程有點抽象,總結來說:只有一個線程執行同步代碼塊的時候,此刻加的鎖為偏向鎖,如果發生其他線程(同時競爭不激烈)來競爭鎖,則鎖升級為輕量級鎖,如果競爭再激烈,則升級為重量級鎖。
輕量級/重量級鎖
輕量級鎖:鎖競爭不激烈的場景下,線程未獲得鎖則保持自旋(不阻塞等待一直嘗試獲取鎖)。
重量級鎖:鎖競爭激烈的場景下,線程未競到鎖則進入阻塞狀態,等待被喚醒,典型:synchronized
鎖升級
Java的synchronized有鎖升級的機制:
synchronized自適應進行升級的過程,保證了JVM不盲目加鎖浪費資源,在鎖競爭緩和的情況下線程不阻塞浪費時間,及時獲取到鎖,在鎖競爭激烈的情況下,讓線程阻塞減輕CPU負擔。
CAS
CAS(Compare And Swap)。顧名思義先比較再交換,CAS涉及到三個數據,這里可以理解為value(修改前讀取到的值;主內存中的共享變量)、exceptValue(上一次修改后保存的值;線程里的局部變量)、swapValue(要修改的值;線程里的局部變量)。定義為exceptValue是因為CAS期待exceptValue修改為swapValue。
CAS的工作機制:
- 讀取內存值(value)。
- 比較 value 和exceptValue。
- 如果相等,則寫入 swapValue,否則CAS失敗。
這三步操作是不可分割的也就是說一次CAS是原子性的。
exceptValue的值在CAS成功后被更新為value,否則保持原值不變。
CAS引發的ABA問題
假設原值為A,要修改為B。如果存在多個線程要執行這個修改操作:
- 一個線程修改成功后,由于緩存一致性協議,value變化時,其他線程已經讀取的value也會被強制刷新為最新值。多個線程進行CAS(A,A,B)時,一個成功后,其他的線程會CAS失敗,不會對A重復CAS(A,A,B)。
- 但是CAS只檢查值,不關心在修改這個值之前是否發生了A->B->…->A這種騷操作。由此引發了ABA問題。例如:
線程t1要將A修改為B,而線程t2要將A修改為B,再修改為A;
如果t2先執行完這兩個CAS操作,t1由于緩存一致性協議會實時讀取到最新值,所以t1在CAS前里面的A變為B再變為A,t1執行CAS,兩次線程結束后最終結果為B。
雖然上面這ABA操作看起來沒問題,但極端情況下卻容易出問題:
我有200塊存款,要取100塊錢;
我來到ATM,插卡輸密碼,設置取100塊再點擊確定,恰好系統超時沒有及時吐錢,我惱怒的再摁了一下,由此產生了兩個線程t1,t2。兩個線程都要CAS(200,200,100),正常情況下,總有一個成功而另一個失敗,但是又恰好老媽打給我100塊生活費,產生線程t3,在t2完成CAS后,t3又來一波CAS(100,100,200)加了100塊,沒有t3,t1應該是CAS(100,200,100)但現在成了CAS(200,200,100),于是ATM再吐100塊。
這是不符合實際的,現實生活即便手癢快速摁幾下,也不會重復響應。
解決方案
為此基于CAS只比較新舊值的特性引入了版本號,版本號是一個單調遞增的常量,每次CAS就+1,這樣即便A->B->A也能因為版本號而察覺到。
原子類
針對多線程操作共享變量例如例如變量自增這種而不發生線程不安全問題,可以使用原子類。原子類底層使用了CAS,可以保證變量操作的原子性,常見的原子類有AtomicInteger、AtomicIntegerArray、AtomicBoolean、AtomicLong、AtomicReference、AtomicStampedReference。舉例AtomicInteger:
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;public class Main {public static void main(String[] args) {AtomicInteger i = new AtomicInteger();i.addAndGet(1); //對于int/Integer i,相當于i+=1System.out.println(i);i.decrementAndGet();//相當于--iSystem.out.println(i);i.getAndDecrement();//相當于i--System.out.println(i);i.incrementAndGet();//相當于++iSystem.out.println(i);i.getAndIncrement();//相當于i++System.out.println(i);}
}
公平/不公平鎖
Java對公平鎖的定義是先來后到,哪個線程先搶鎖就哪個線程拿鎖,所以非公平鎖就是所有鎖競爭者獲得鎖的機會均等(操作系統隨機調度線程)。
可重入鎖
一個線程執行到同步代碼塊給自己加了鎖,在這個代碼塊里又給自己加了鎖,造成了死鎖,這個線程就自己阻塞自己,鎖也釋放不了。如果允許線程給自己多次加鎖,但又不會發生死鎖,同步代碼塊執行完后鎖正常釋放,那就是可重入鎖。
學Java的同學不用擔心死鎖的問題,只要以Reentrant開頭命名的鎖都是可重入鎖,而且JDK提供的所有現成的Lock實現類,包括synchronized關鍵字鎖都是可重入的。
ReentrantLock
和synchronized同級別,是在Java5以后引入的,相比于synchronized對鎖的使用更靈活,也更復雜。以下是同synchronized的對比:
synchronized | ReentrantLock | |
---|---|---|
實現方式 | synchronzied是關鍵字,實現機制在JVM內部,需要手動解鎖 | ReentrantLock是Java.util.concurrent.locks.ReentrantLock里的類,是在JVM外部實現的,而ReentrantLock需要手動unlock()解鎖 |
鎖競爭處理 | 線程鎖競爭失敗會一直阻塞 | 不會阻塞;可以通過trylock(TIME)/trylock()返回一個boolean值,如果是false代表競爭鎖失敗,調用者可根據這個判斷編寫競爭失敗的代碼邏輯 |
鎖特性 | 非公平鎖 | 默認非公平鎖,可通過構造方法傳入ture成為公平鎖 |
等待和喚醒 | 可通過Object類的wait()、notify()進行等待和喚醒,但喚醒目標是隨機的 | 可通過Condition類指定喚醒某個線程 |
常用方法lock()、trylock(Time)、unclock()。
import java.util.concurrent.locks.ReentrantLock;public class Main {public static void main(String[] args) {ReentrantLock locker=new ReentrantLock();Thread t1=new Thread(()->{for(int i=0; i<500; i++){try{locker.lock();System.out.println(Thread .currentThread().getName()+i);}finally{locker.unlock();//未防止忘記釋放鎖,將其放在finally里}}});t1.start();}
}
讀寫鎖
多個線程對共享變量的讀取是不會發生線程不安全的的,如果有寫入的情況才會發生:
- 所有線程只讀,線程安全
- 所有線程寫入,線程不安全
- 有讀有寫,線程不安全
當有多個線程操作一個共享變量時,對其加鎖且對它的讀取是不互斥,但是寫入時只能一個線程持有鎖,達到:
- 讀與讀之間不互斥
- 所有線程寫入,一個線程持有鎖其他阻塞
- 有讀有寫,一個線程持有鎖其他阻塞
于是我們引入了讀寫鎖ReentrantReadWriteLock類,通過ReentrantReadWriteLock.readLock獲取一個讀鎖ReadLock對象;通過ReentrantReadWriteLock.writeLock獲取一個寫鎖WriteLock對象。
這兩個對象使用lock()、tryLock()、**unlock()**來加鎖釋放鎖。
import java.util.concurrent.locks.ReentrantReadWriteLock;public class Main {static int i=0;public static void main(String[] args) {ReentrantReadWriteLock locker=new ReentrantReadWriteLock();Thread t1=new Thread(()->{locker.readLock().lock();//readLock是ReentrantReadWriteLock類里RreadLock類型的字段System.out.println(i);locker.readLock().unlock();locker.writeLock().lock();//writeLock是ReentrantReadWriteLock類里WriteLock類型的字段i+=1;locker.writeLock().unlock();});t1.start();}
}
Callable接口
Callable類似于Runnable接口,里面定義了call()方法,同run()方法一樣對任務進行了包裝。
但不同的是Callable接口帶有泛型,且call()方法帶有返回值,且不可直接將實現了Callable接口的類對象作為參數直接傳入Thread類的構造方法里,需要通過FutureTask類將任務提交到線程里。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Main {public static void main(String[] args) throws InterruptedException, ExecutionException {//返回一個實現了Callable接口的匿名類對象Callable<Integer> callable = (() -> {int sum = 0;for (int i = 1; i <= 100; i++) {sum += i;}return sum;});FutureTask<Integer> futureTask = new FutureTask<>(callable);//FutureTask實現了RunnableFuture接口,RunnableFuture繼承了Runnable接口Thread t = new Thread(futureTask);t.start();System.out.println(futureTask.get());}
}
如果要實現上面同樣的功能,還需在Main里定義一個static字段類記錄sum的值方便任務執行完后來確定任務效果。而通過call()帶返回值的屬性和new FutureTask.get()方法得到任務結果,將任務和線程分開,達到高內聚低耦合的目的。
完。