來一段很常見的死鎖代碼,當個開胃菜:
class Deadlock {
public static String str1 = "str1";
public static String str2 = "str2";
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
while (true) {
synchronized (Deadlock.str1) {
System.out.println(Thread.currentThread().getName() + "鎖住 str1");
Thread.sleep(1000);
synchronized (Deadlock.str2) {
System.out.println(Thread.currentThread().getName() + "鎖住 str2");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
while (true) {
synchronized (Deadlock.str2) {
System.out.println(Thread.currentThread().getName() + "鎖住 str2");
Thread.sleep(1000);
synchronized (Deadlock.str1) {
System.out.println(Thread.currentThread().getName() + "鎖住 str1");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
}
}
複制
猜猜上面的輸出是啥?如果我将str2也指派成”str1“,又會如何呢?
Java中鎖的分類隻是将鎖的特性進行了歸納,可以分為:
- 可重入鎖/不可重入鎖
- 可中斷鎖
- 公平鎖/非公平鎖
- 獨享鎖(互斥鎖)/共享鎖(讀寫鎖)
- 樂觀鎖/悲觀鎖
- 分段鎖
- 偏向鎖/輕量級鎖/重量級鎖
- 自旋鎖
注意:ReentrantLock和ReentrantReadWriteLock雖然一些性質相同,但前者實作的是Lock接口,後者實作的是ReadWriteLock接口。
1. 可重入鎖/不可重入鎖
可重入鎖
可重入鎖是指在同一個線程在外層方法擷取鎖的時候,在進入内層方法會自動擷取鎖。 ReentrantLock和synchronized都是可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖。
看下面這段代碼就明白了:
synchronized void method1() throws Exception{
Thread.sleep(1000);
method2();
}
synchronized void method2() throws Exception{
Thread.sleep(1000);
}
複制
上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,線程A執行到了method1,此時線程A擷取了這個對象的鎖,而由于method2也是synchronized方法,假如synchronined不具備可重入性,此時線程A需要重新申請鎖,這就會造成一個問題,因為線程A已經持有了該對象的鎖,而又在申請擷取該對象的鎖,這樣就會線程A一直等待永遠不會擷取到的鎖。
不可重入鎖
不可重入鎖是指若目前線程執行某個方法已經擷取了該鎖,那麼在方法中嘗試再次擷取鎖時,就會擷取不到且被阻塞。我們嘗試設計一個不可重入鎖:
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
複制
使用該鎖:
public class Count{
Lock lock = new Lock();
public void print(){
lock.lock();
doAdd();
lock.unlock();
}
public void doAdd(){
lock.lock();
//do something
lock.unlock();
}
}
複制
目前線程執行print()方法首先擷取lock,接下來執行doAdd()方法就無法執行doAdd()中的邏輯,必須先釋放鎖。這個例子很好的說明了不可重入鎖。
2.可中斷鎖
可中斷鎖:顧名思義,就是可以相應中斷的鎖。
在Java中,synchronized是不可中斷鎖,而ReentrantLock是可中斷鎖。
如果某一線程A正在執行鎖中的代碼,另一線程B正在等待擷取該鎖,可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在别的線程中中斷它,這種就是可中斷鎖。
可以檢視ReentrantLock的lockInterruptibly(),已經充分展現了Lock的可中斷性。點選檢視更多細節
3.公平鎖/非公平鎖
公平鎖:以請求鎖的順序來擷取鎖。比如同時有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該鎖,這種就是公平鎖。
非公平鎖:無法保證鎖的擷取是按照請求鎖的順序進行的。這樣就可能導緻某個或者一些線程永遠擷取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的線程擷取鎖的順序;而對于ReentrantLock和ReentrantReadWriteLock,預設情況下是非公平鎖,但是可以在構造函數中設定為公平鎖。
在ReentrantLock中定義了2個靜态内部類,一個是NotFairSync,一個是FairSync,分别用來實作非公平鎖和公平鎖。
4.獨享鎖(互斥鎖)/共享鎖(讀寫鎖)
獨享鎖:該鎖一次隻能被一個線程所持有。
共享鎖:該鎖可被多個線程所持有。
對于ReentrantLock/synchronized而言,其是獨享鎖。但是對于ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。
讀鎖的共享鎖可保證并發讀是非常高效的,但讀寫、寫讀 、寫寫的過程是互斥的。
5. 樂觀鎖/悲觀鎖
樂觀鎖與悲觀鎖是從看待并發同步的角度來劃分的。
悲觀鎖認為對于同一個資料的并發操作,一定是會發生修改的,哪怕沒有修改,也會認為修改。是以對于同一個資料的并發操作,悲觀鎖采取加鎖的形式。悲觀的認為,不加鎖的并發操作一定會出問題。
樂觀鎖則認為對于同一個資料的并發操作,是不會發生修改的。在更新資料的時候,會采用嘗試更新,不斷重新的方式更新資料。樂觀的認為,不加鎖的并發操作是沒有事情的。 從上面的描述我們可以看出,悲觀鎖适合寫操作非常多的場景,樂觀鎖适合讀操作非常多的場景,不加鎖會帶來大量的性能提升。
悲觀鎖在Java中的使用,就是J.U.C下的locks包和synchronized關鍵字。
樂觀鎖在Java中的使用,(又稱為無鎖程式設計)常常采用的是CAS算法,J.U.C下Atomic包的各種實作。
6. 分段鎖
分段鎖其實是一種鎖的設計,對于ConcurrentHashMap而言,其并發的實作就是通過分段鎖的形式來實作高效的并發操作。
我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱為Segment,它是類似于HashMap(JDK7版本及以上的HashMap實作)的結構,即内部擁有一個Entry數組,數組中的每個元素又是一個連結清單;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
當需要put元素的時候,并不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然後對這個分段進行加鎖,是以當多線程put的時候,隻要不是放在一個分段中,就實作了真正的并行的插入。
在統計size的時候,可就是擷取hashmap全局資訊的時候,就需要擷取所有的分段鎖才能統計。
分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。
7. 偏向鎖/輕量級鎖/重量級鎖
這三種鎖對應synchronized鎖的三種狀态。JDK6通過引入鎖更新的機制來實作高效synchronized。這三種鎖的狀态是通過對象螢幕在對象頭中MarkWord的值來表明的。
偏向鎖是指一段同步代碼一直被一個線程所通路,那麼該線程會自動擷取鎖。降低擷取鎖的代價。
輕量級鎖是指當鎖是偏向鎖的時候,被另一個線程所通路,偏向鎖就會更新為輕量級鎖,其他線程會通過自旋的形式嘗試擷取鎖,不會阻塞,提高性能。
重量級鎖是指當鎖為輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有擷取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓其他申請的線程進入阻塞,性能降低。
有關synchronized的這三種鎖狀态變化的詳解,點選檢視更多細節
8. 自旋鎖
在Java中,自旋鎖是指當一個線程在擷取鎖的時候,如果鎖已經被其它線程擷取,那麼該線程将循環等待,然後不斷的判斷鎖是否能夠被成功擷取,直到擷取到鎖才會退出循環。
自旋鎖存在的問題:
- 如果某個線程持有鎖的時間過長,就會導緻其它等待擷取鎖的線程進入循環等待,消耗CPU。使用不當會造成CPU使用率極高。
- 上面Java實作的自旋鎖不是公平的,即無法滿足等待時間最長的線程優先擷取鎖。不公平的鎖就會存在“線程饑餓”問題。
自旋鎖的優點:
- 自旋鎖不會使線程狀态發生切換,一直處于使用者态,即線程一直都是active的;不會使線程進入阻塞狀态,減少了不必要的上下文切換,執行速度快
- 非自旋鎖在擷取不到鎖的時候會進入阻塞狀态,進而進入核心态,當擷取到鎖的時候需要從核心态恢複,需要線程上下文切換。 (線程被阻塞後便進入核心(Linux)排程狀态,這個會導緻系統在使用者态與核心态之間來回切換,嚴重影響鎖的性能)
在JDK6之後,自旋鎖進行了優化變成自适應自旋鎖了。點選檢視更多細節