天天看點

一網打盡Java中鎖的分類1. 可重入鎖/不可重入鎖2.可中斷鎖3.公平鎖/非公平鎖4.獨享鎖(互斥鎖)/共享鎖(讀寫鎖)5. 樂觀鎖/悲觀鎖6. 分段鎖7. 偏向鎖/輕量級鎖/重量級鎖8. 自旋鎖

來一段很常見的死鎖代碼,當個開胃菜:

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中鎖的分類隻是将鎖的特性進行了歸納,可以分為:

  1. 可重入鎖/不可重入鎖
  2. 可中斷鎖
  3. 公平鎖/非公平鎖
  4. 獨享鎖(互斥鎖)/共享鎖(讀寫鎖)
  5. 樂觀鎖/悲觀鎖
  6. 分段鎖
  7. 偏向鎖/輕量級鎖/重量級鎖
  8. 自旋鎖
注意: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中,自旋鎖是指當一個線程在擷取鎖的時候,如果鎖已經被其它線程擷取,那麼該線程将循環等待,然後不斷的判斷鎖是否能夠被成功擷取,直到擷取到鎖才會退出循環。

自旋鎖存在的問題:

  1. 如果某個線程持有鎖的時間過長,就會導緻其它等待擷取鎖的線程進入循環等待,消耗CPU。使用不當會造成CPU使用率極高。
  2. 上面Java實作的自旋鎖不是公平的,即無法滿足等待時間最長的線程優先擷取鎖。不公平的鎖就會存在“線程饑餓”問題。

自旋鎖的優點:

  1. 自旋鎖不會使線程狀态發生切換,一直處于使用者态,即線程一直都是active的;不會使線程進入阻塞狀态,減少了不必要的上下文切換,執行速度快
  2. 非自旋鎖在擷取不到鎖的時候會進入阻塞狀态,進而進入核心态,當擷取到鎖的時候需要從核心态恢複,需要線程上下文切換。 (線程被阻塞後便進入核心(Linux)排程狀态,這個會導緻系統在使用者态與核心态之間來回切換,嚴重影響鎖的性能)

在JDK6之後,自旋鎖進行了優化變成自适應自旋鎖了。點選檢視更多細節