作者:千珏
前言
每次面試的時候總是有面試官會甩出致命三連 高併發、高可用、高性能
我們又稱其爲程序員三高,今天千珏本珏講的就是三高中的高併發中的“鎖”事。
首先我們要知道java中要有哪些鎖,下面這張圖千珏認爲還是能很清楚的說明java鎖之間的區別的(圖片來自於網絡,如果有侵權,請通過郵箱聯繫我刪除)
下面千珏就帶你來一一過下java中的“鎖”事
悲觀鎖和樂觀鎖
悲觀鎖的概念:總是假設最壞的情況,每次拿數據都認爲別人會修改數據,所以要加鎖,別人只能等待,直到我釋放鎖才能拿到鎖;數據庫的行鎖、表鎖、讀鎖、寫鎖都是這種方式。java中的synchronized和Lock的實現類也是悲觀鎖的思想。
樂觀鎖的概念:總是假設最好的情況,每次拿數據都認爲別人不會修改數據,所以不會加鎖,但是更新的時候,會判斷在此期間有沒有人修改過;一般基於版本號機制實現。java中的樂觀鎖最常見的是CAS算法。
根據上面的概念我們可以簡單得知樂觀鎖和悲觀鎖的應用場景
- 樂觀鎖適用於讀多寫少的情況,因爲不加鎖直接讀可以讓系統的性能大幅度的提高 。
- 悲觀鎖適用於寫多讀少的情況,因爲等待到鎖被釋放後,可以立即獲得鎖進行操作。
直接說概念有可能會有點懵,我們來看下java中的調用方式
//悲觀鎖用synchronized實現
public synchronized void test(){//執行相應的操作}
//悲觀鎖用Lock實現
Lock lock = new ReentrantLock();
public void testLock(){
lock.lock();
//TODO 執行相應的操作
lock.unlock();
}
//樂觀鎖
AtomicInteger atomicInteger = new AtomicInteger();
public void testCAS(){
atomicInteger.incrementAndGet();
}
看到以上的調用方式我們可以看出來悲觀鎖都是直接加鎖來保證資源的同步,這時候很多朋友就會問了爲什麼樂觀鎖沒加鎖也能實現資源同步呢,是呀,爲什麼呢,且看千珏的分析。
爲什麼樂觀鎖沒加鎖也能實現資源同步呢?
我們開頭就說了因爲樂觀鎖最主要的實現方式是CAS算法。
CAS就是Compare and Swap,即比較再交換,jdk5中增加了併發包java.util.concurrent.*,其下面的類使用CAS算法實現了區別於synchronouse同步鎖的一種樂觀鎖。
CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。
就是這個CAS可以讓我們用無鎖的方式實現“鎖”,CAS雖然很強,但是也存在着幾個問題
-
ABA問題。因爲CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。
從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。
關於ABA問題參考文檔: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html
-
循環時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。
-
只能保證一個共享變量的原子操作。當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。
從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行CAS操作。
自旋鎖和適應性自旋鎖
自旋鎖:爲了避免線程在獲取同步資源時,線程的頻繁掛起和恢復,可以讓原本需要等待的線程一直循環的獲得鎖,這就是自旋鎖。
適應性自旋鎖:自適應自旋鎖的自適應反映在自旋的時間不在固定了。如果在同一個鎖對象上,自旋線程之前剛剛獲得過鎖,且現在持有鎖的線程正在運行中,那麼虛擬機會認爲這次自旋也很有可能會成功,進而允許該線程等待持續相對更長的時間,比如100個循環。反之,如果某個鎖自旋很少獲得過成功,那麼之後再獲取鎖的時候將可能省略掉自旋過程,以避免浪費處理器資源。
以上概念來源於網絡。
自旋鎖的缺點:自旋等待雖然避免了線程切換的開銷,但它要佔用處理器時間。如果鎖被佔用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被佔用的時間很長,那麼自旋的線程只會白浪費處理器資源。所以,自旋等待的時間必須要有一定的限度,如果自旋超過了限定次數(默認是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應當掛起線程。JDK6中默認開啓自旋鎖。
自旋鎖實現的原理同樣也是CAS, 上面也說了樂觀鎖的實現原理是CAS可以達到無鎖的方式來上鎖,自旋鎖呢 就是要自旋加個無限循環直到他的值改變成功。
自旋鎖中有三種常見的鎖形式:TicketLock、CLHlock和MCSlock.(如果有想要了解的朋友留言給我,我單獨開篇單章)
無鎖和偏向鎖和輕量級鎖和重量級鎖
這四種鎖實際上是鎖的四種狀態,這個時候我相信肯定又要有讀者問了鎖的狀態是什麼,鎖是存在哪裏的呢。
別急別急,跟着千珏走,offer拿到手抽筋。
鎖是存在哪裏的呢?
鎖存在Java對象頭中的Mark Word。Mark Word默認不僅存放鎖標誌位,還存放對象hashCode等信息。運行時,會根據鎖的狀態 ,修改Mark Work的存儲內容。如果對象是數組類型,則虛擬機用3個字寬存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,一字寬等於四字節,即32bit.關於對象頭等相關知識,可以參考Java虛擬機相關文章。
Mark Word裏面有關於鎖的內容
存儲內容 | 標誌位 | 狀態 |
---|---|---|
對象hashcode、對象分代年齡、是否是偏向鎖(0) | 01 | 無鎖 |
指向鎖記錄的指針 | 00 | 輕量級鎖 |
指向重量級鎖的指針 | 10 | 重量級鎖 |
偏向線程ID、偏向時間戳、對象分代年齡,是否是偏向鎖(1) | 01 | 偏向鎖 |
無鎖:就是不上鎖,不對資源進行鎖定,使得所有的線程都能訪問資源,但是同時只有一個資源 能修改成功。
偏向鎖:線程在大多數情況下並不存在競爭條件,使用同步會消耗性能,而偏向鎖是對鎖的優化,可以消除同步,提升性能。當一個線程獲得鎖,會將對象頭的鎖標誌位設爲01,進入偏向模式.偏向鎖可以在讓一個線程一直持有鎖,在其他線程需要競爭鎖的時候,再釋放鎖。
輕量級鎖:當線程1獲得偏向鎖後,線程2進入競爭狀態,需要獲得線程1持有的鎖,那麼偏向鎖就會升級爲輕量級鎖,其他 線程會通過自旋的形式嘗試獲取鎖。
重量級鎖:當自旋超過一定的次數,或者一個線程在持有鎖,一個線程在自旋,又有第三個來訪時,輕量級鎖升級爲重量級鎖,此時等待鎖的線程都會進入阻塞狀態。
整體鎖狀態升級流程爲:偏向鎖 ----> 輕量級鎖 ----> 重量級鎖
公平鎖和非公平鎖
公平鎖:就是每個線程都能拿到鎖。
非公平鎖:不能保證每個線程都能拿到鎖。
有語言描述看的話有點懵,舉個例子。
公平鎖就是你去食堂打飯的時候如果老老實實排隊打飯的話就是公平鎖。
非公平鎖就是你去食堂打飯的時候可以不用排隊,前面一個人如果打好飯了,你可以直接打飯,不用管還有多少人沒打飯。這樣的話就是非公平鎖。
java當中的公平鎖,非公平鎖實現
//公平鎖
ReentrantLock lock = new ReentrantLock(true);
//非公平鎖
ReentrantLock lock = new ReentrantLock(false);
具體原理就不探討了,如果想要知道爲什麼這樣實現的,可以留言給我。
適用場景就是:線程佔用時間要長於線程切換時間的還是用公平鎖好一些,反之用非公平鎖好一些。
可重入鎖和非可重入鎖
可重入鎖就是可重複調用的鎖,在外面方法使用鎖之後,在裏面依然可以使用鎖,並且不發生死鎖(前提是同一個對象或者class),這樣的鎖就叫做可重入鎖。synchronized和ReentrantLock都是可重入鎖。
看着概念你有可能 有點懵,但是看實現就覺得這玩意很簡單了。
public class Test implements Runnable{
public static void main(String []args){
Test test = new Test();
for(int i = 0; i < 5; i++){
new Thread(test).start();
}
}
@Override
public void run() {
out();
}
public synchronized void out(){
System.out.println(Thread.currentThread().getName());
in();
}
public synchronized void in(){
System.out.println(Thread.currentThread().getName());
}
}
//輸出結果如下,可以看出線程是沒有阻塞的
Thread-2
Thread-2
Thread-4
Thread-4
Thread-3
Thread-3
Thread-1
Thread-1
Thread-0
Thread-0
不可重入鎖就是不可重複調用的鎖,在外面方法使用鎖之後,在裏面就不能使用鎖了,這個時候鎖會阻塞直到你外面的鎖釋放後纔會獲得裏面的鎖。會產生死鎖這種情況 。
不可重入鎖實現如下:
public class NoLock {
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException {
while (isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
獨享鎖和共享鎖
獨享鎖:該鎖每一次只能被一個線程所持有,synchronized以及ReentrantLock都是獨享鎖
共享鎖:該鎖可被多個線程共有。獲得共享鎖的線程只能讀數據,不能修改數據。
ReentrantReadWriteLock有兩把鎖:ReadLock和WriteLock,由詞知意,一個讀鎖一個寫鎖,合稱“讀寫鎖”。讀鎖是共享鎖,寫鎖是獨享鎖
總結
java"鎖"事到此就結束了,有很多原理方面的東西,千珏沒有深入的介紹,一方面由於是本人水平不太夠,一方面由於篇幅的問題,如果讀者看完對此篇文章有什麼疑問的地方都可以留言告訴我。
最後,求求大家看到這篇文章覺得寫的還行的,麻煩麻煩你們的小手點個關注吧,點個贊吧,你們的贊和關注是千珏寫作的動力。
2019完結,希望2020能更好吧,祝大家元旦快樂。