java中的鎖介紹

本節內容:

java鎖介紹

偏向鎖、輕量級鎖、重量級鎖

這三種鎖特指synchronized鎖的狀態,經過對象頭中的mark work字段表示鎖狀態。java

偏向鎖:程序員

自始至終,對這把鎖都不存在競爭,只須要作個標記,這就是偏向鎖,每一個對象都是一個內置鎖(內置鎖是可重入鎖),一個對象被初始化後,尚未任何線程來獲取它的鎖時,那麼它就是可偏向的,當有線程來訪問並嘗試獲取鎖的時候,他就會把這個線程記錄下來,之後若是獲取鎖的線程正式偏向鎖的擁有者,就能夠直接得到鎖,偏向鎖性能最好。編程

輕量級鎖:安全

輕量級鎖是指原來是偏向鎖的時候,這時被另一個線程訪問,存在鎖競爭,那麼偏向鎖就會升級爲輕量級鎖,線程會經過自旋的形式嘗試獲取鎖,而不會陷入阻塞。微信

重量級鎖:併發

重量級鎖是互斥鎖,主要是利用操做系統的同步機制實現的,當多個線程直接有併發訪問的時候,且鎖競爭時間長的時候,輕量級鎖不能知足需求,鎖就升級爲重量級鎖,重量級鎖會使得其餘拿不到鎖的線程陷入阻塞狀態,重量級鎖的開銷相對較大。app

可重入鎖、非可重入鎖

可重入鎖:dom

可重入鎖的意思是若是當前線程已經持有了這把鎖,能再不釋放的鎖的狀況下再次得到這把鎖,若是一個線程試圖獲取它已經持有的鎖,那麼這個請求會成功,每一個鎖關聯一個獲取計數值和一個全部者線程,當計數值爲0的時候,認爲沒有線程持有該鎖,當線程請求一個未被持有的鎖的時候,JVM將記下鎖的持有者,而且計數值置爲1,若是同一個線程再次獲取該鎖的時候,計數值將遞增,jvm

不可重入鎖:ide

同理不可重入鎖就是指雖然當前線程已經持有了這把鎖,可是若是想要再次得到這把鎖,必需要先釋放鎖後才能再次嘗試獲取。

共享鎖、獨佔鎖

共享鎖:

共享鎖就是咱們能夠同一把鎖被多個線程同時得到,最典型的就是讀寫鎖中的讀鎖。

獨佔鎖:

同理,獨佔鎖就是線程只能對一個線程持有,類比讀寫鎖中的寫鎖,

公平鎖、非公平鎖

公平鎖:

公平鎖就是若是線程當前拿不到這把鎖,那麼線程就都會進入等待,開始排隊,在等待隊列裏等待時間長的線程就會優先拿到這把鎖,先來先得。

非公平鎖:

非公平鎖是指在必定的狀況下,某些線程會忽略掉已經在排隊的線程,發生插隊的狀況。

悲觀鎖、樂觀鎖

悲觀鎖:

悲觀鎖顧名思義,比較悲觀,悲觀鎖認爲若是不鎖住這個共享資源,別的線程就回來競爭,就會形成數據結果錯誤,因此在獲取共享資源前,必需要先拿到鎖,以便達到「獨佔」的狀態,,讓其餘線程沒法訪問該數據,這樣就不會發生數據錯誤。常見的悲觀鎖例如synchronized關鍵字、Lock接口

樂觀鎖:

同理樂觀鎖是相對悲觀鎖而言的,樂觀鎖就是比較樂觀了,它認爲通常狀況下數據不會發生衝突,只有在數據進行更新的時候,纔會對比數據在被當前線程更新期間有咩有被修改過,若是沒有被修改過,則能夠正常更新數據,若是數據發生過修改和預期不同,那麼本次更新操做就不能成功,因此能夠放棄此次更新或者選擇報錯、重試等策略。常見的樂觀鎖例如:各類原子類

自旋鎖、非自旋鎖

自旋鎖:

自旋鎖是指若是線程拿不到鎖,那麼線程並不直接陷入阻塞或者釋放CPU資源而是開始自我旋轉,不停的嘗試獲取鎖,這個循環的過程成爲自旋。

非自旋鎖:

非自旋鎖就是沒有自旋的過程,若是拿不到鎖就直接放棄或者進行其餘邏輯處理,好比排隊、阻塞。

可中斷鎖、不可中斷鎖

可中斷鎖:

可中斷鎖指在獲取鎖的過程當中,能夠中斷鎖以後去作其餘事情,不須要一直等到獲取到鎖才繼續處理

不可中斷鎖:

synchronized是不可中斷鎖,就是指一旦申請了鎖,只能等到拿到鎖之後才能進行其餘的邏輯處理。

synchronized鎖介紹

什麼是synchronized鎖

java中每一個對象中都持有一把鎖與之關聯,控制着對象的synchronized代碼,想要執行對象的synchronized代碼,
必須先得到對象的鎖,這個鎖就是對象的Monitor鎖,synchronized實現加鎖解鎖就是利用Monitor鎖實現的。
獲取Monitor鎖的惟一方式是進入由這個鎖保護的同步代碼塊或者同步方法中,線程進入synchronized保護的代碼以前得到鎖,
而後在正常執行代碼完成後或者異常退出,都會自動釋放鎖。

synchronized關鍵字在同步代碼塊中的應用:
咱們經過分析一下代碼的反彙編內容來理解synchronized是如何利用monitor鎖來工做的
咱們先來分析同步代碼塊的反彙編內容

public class TestSync {
    public void sync1(){
        synchronized (this){
            int ss = 10;
            System.out.println(ss);
        }
    }
}

如上圖代碼,咱們定義的TestSync類中的sync1()方法中包含一個同步代碼塊,咱們經過指令:javap -verbose TestSync.class查看方法對應的反彙編內容以下:

public void sync1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter    //加鎖
         4: bipush        10
         6: istore_2
         7: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_2
        11: invokevirtual #3   // Method java/io/PrintStream.println:(I)V
        14: aload_1
        15: monitorexit    //解鎖
        16: goto          24
        19: astore_3
        20: aload_1
        21: monitorexit    解鎖
        22: aload_3
        23: athrow
        24: return

上述中我能夠看出同步代碼塊實際上多了monitorenter和monitorexit指令,
咱們能夠理解爲對應的加解鎖,之因此有一個monitorenter對應兩個monitorexit是由於jvm要
保證每一個monitorenter必須有與之對應的monitorexit,那麼就須要在正常結束流程和異常結束流程中
分別執行monitorexit以保證正常或者拋出異常狀況下都能釋放鎖。

monitorenter含義:
每一個對象維護着一個計數器,沒有被鎖定的對象計數器爲0,執行monitorenter的線程嘗試獲取monitor的全部權,那麼會有如下三種狀況:
若是該monitor的計數爲0,則線程得到該monitor後並將其計數設置成1,而後該線程就該monitor的持有者。若是線程已經獲取了該monitor,那麼該monitor的計數將累加。
若是線程已是其餘線程已經獲取了該monitor,那麼當前想要獲取該monitor的線程會被阻塞,知道該monitor的計數爲0的時候,表明該monitor已經被釋放了,而後當前線程就能夠嘗試獲取該monitor了。

monitorexit含義:
monitorexit的做用是將monitor的計數減1,知道減爲0爲止,表明這個monitor已經被釋放了,
那麼此時其它線程就能夠嘗試獲取該monitor的全部權了。

synchronized關鍵字在同步方法中的應用:

咱們再來看看同步方法反彙編後的內容又是怎麼樣的,咱們對一下內容執行反彙編。

public class TestSync {
    public synchronized void sync2(){
        int aa = 10;
        System.out.println(aa);
    }
}

反彙編代碼以下:

public synchronized void sync2();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=2, args_size=1
         0: bipush        10
         2: istore_1
         3: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: iload_1
         7: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        10: return

從上述代碼中咱們能夠看出同步方法和同步代碼塊的不一樣之處是同步代碼塊
中依賴monitorenter和monitorexit來加解鎖,同步方法中會多出一個
ACC_SYNCHRONIZED的flags修飾符,來代表他是同步方法。因此被synchronized修飾的方法會有一
個ACC_SYNCHRONIZED標誌,那麼當線程要訪問某個方法的時候就會檢查方法是否攜帶ACC_SYNCHRONIZED標誌,
帶的話就先去獲取monitor鎖,而後獲取到鎖後在執行方法,方法執行完後釋放monitor鎖。

synchronized關鍵字和Lock接口對比

相同點:

  • synchronized和Lock都是用來保護資源線程安全的。
  • 都保證可見性,對於synchronized而言,線程A進入synchronized代碼塊或者方法中進行的操做,對於後續的得到同一個monitor鎖的線程B是可見的。同理對於Lock而言,他和synchronized是同樣的均可以保證可見行
  • synchronized和ReentrantLock都擁有可重入的特色

不一樣點:

  • 用法區別:synchronized關鍵字能夠加在方法上不須要指定鎖對象,也能夠新建一個同步代碼塊而且自定義monitor鎖對象,而Lock接口必須顯示用Lock鎖對象開始加鎖lock()和解鎖unLock(),而且通常會在finally塊中確保用unLock()來解鎖以防止發生死鎖。
  • 加解鎖順序不一樣,Lock能夠不按照加鎖順序進行解鎖好比咱們先獲取A鎖,在獲取B鎖,那麼解鎖時咱們能夠先解鎖A鎖在解鎖B鎖,可是synchronized的加解鎖必須有順序,好比獲取A鎖,在獲取B鎖,那麼解鎖就是先解鎖B鎖,在解鎖A鎖。
  • synchronized相比Lock不夠靈活,synchronized鎖一旦被某個線程獲取了,那麼其它線程只能阻塞等待釋放鎖,若是持有鎖的線程執行好久那麼整個程序的運行效率就會很低,並且若是一直不釋放鎖其餘線程將一直等待下去,相比Lock的lockInterruptibly方法,若是以爲持有鎖的線程執行過久了能夠中斷退出,還能夠用tryLock()嘗試獲取鎖,獲取不到就去執行別的邏輯。
  • Lock接口的一些實現類例如讀寫鎖中的讀鎖能夠被多個線程持有,synchronized只能被一個線程持有
  • synchronized是內置鎖(Monitor鎖),有JVM實現加解鎖,還分爲偏向鎖、輕量級鎖、重量級鎖,Lock接口根據實現不一樣有不一樣的底層原理。
  • Lock能夠設置是否公平鎖,synchronized不能夠設置
  • java6之後jdk對synchronized進行了不少優化,因此synchronized性能並不比Lock差

公平鎖和非公平鎖

公平鎖和非公平鎖

公平鎖是指線程按照請求順序來分配鎖,非公平鎖是指不徹底按照線程請求順序分配鎖,可是非公平鎖並非徹底的隨機分配而是」在合適的時機「插隊執行。

什麼是合適的時機
所謂合適的時機就是好比新來一個線程要獲取鎖,恰巧上一個持有鎖線程正好執行完畢釋放了鎖,那麼此時這個新來的線程能無論後面排隊的線程而選擇當即插隊執行,可是若是上一個線程還未釋放鎖,那麼新來的線程就須要去等待隊列排隊。
爲何設置非公平鎖
之因此設計非公平鎖是由於相比公平鎖排隊執行,上一個線程釋放鎖後須要先喚醒下一個要執行的線程,而後去獲取鎖在執行,而採用非公平鎖下,就能夠上一個線程釋放了鎖,剛來一個新線程直接獲取鎖就插隊去執行代碼了,不須要額外的喚醒線程成本,並且也有可能在線程喚醒的這段時間內,插隊線程已經獲取鎖而且執行完任務並釋放了鎖。
因此設置非公平鎖,這樣設計的緣由是爲了提升系統總體的運行效率,並且ReentrantLock默認的是非公平鎖。

公平鎖和非公平鎖效果展現

公平鎖和非公平鎖經過設置ReentrantLock中boolean值來設置公平非公平鎖,以下代碼所示是設置爲非公平鎖。

Lock lock=new ReentrantLock(false);

公平鎖代碼展現:

/**
 * 描述:演示公平鎖,分別展現公平和不公平的狀況,
 * 非公平鎖會讓如今持有鎖的線程優先再次獲取到鎖。
 * 代碼借鑑自Java併發編程實戰手冊2.7
 */
​
public class FairAndNoFair {
    public static void main(String[] args) {
        PrintQueue printQueue = new PrintQueue();
​
        Thread[] threads= new Thread[10];
        for(int i=0;i<10;i++){
            threads[i] = new Thread(new Job(printQueue),"Thread "+ i);
        }
​
        for (int i = 0; i < 10; i++) {
            threads[i].start();
            try {
                Thread.sleep(100);//爲了保證執行順序
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Job implements Runnable{
    private PrintQueue printQueue;
    public Job(PrintQueue printQueue){
        this.printQueue=printQueue;
    }
    @Override
    public void run() {
        System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());
        printQueue.printJob();
        System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
    }
}
public class PrintQueue {
    private final Lock lock=new ReentrantLock(false);
​
    public void printJob(){
        lock.lock();
​
        try{
            Long duration = (long) (Math。random()*10000);
            System.out.printf("%s:First PrintQueue: Printing a Job during %d seconds\n",
                    Thread.currentThread().getName(), (duration / 1000));
            Thread.sleep(duration);
        } catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
​
​
        lock.lock();
        try{
            Long duration = (long) (Math.random()*10000);
            System.out.printf("%s:Second PrintQueue: Printing a Job during %d seconds\n",
                    Thread.currentThread().getName(), (duration / 1000));
            Thread.sleep(duration);
        } catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

咱們先運行公平鎖的打印結果以下:

Thread 0: Going to print a job
Thread 0:First PrintQueue: Printing a Job during 9 seconds
Thread 1: Going to print a job  //線程1-9進入等待隊列排隊
Thread 2: Going to print a job
Thread 3: Going to print a job
Thread 4: Going to print a job
Thread 5: Going to print a job
Thread 6: Going to print a job
Thread 7: Going to print a job
Thread 8: Going to print a job
Thread 9: Going to print a job
Thread 1:First PrintQueue: Printing a Job during 5 seconds//線程0執行完釋放了鎖,線程1開始執行
Thread 2:First PrintQueue: Printing a Job during 1 seconds
Thread 3:First PrintQueue: Printing a Job during 9 seconds
Thread 4:First PrintQueue: Printing a Job during 7 seconds
Thread 5:First PrintQueue: Printing a Job during 8 seconds
Thread 6:First PrintQueue: Printing a Job during 5 seconds
Thread 7:First PrintQueue: Printing a Job during 2 seconds
Thread 8:First PrintQueue: Printing a Job during 9 seconds
Thread 9:First PrintQueue: Printing a Job during 7 seconds
Thread 0:Second PrintQueue: Printing a Job during 0 seconds
Thread 1:Second PrintQueue: Printing a Job during 6 seconds
Thread 0: The document has been printed
Thread 1: The document has been printed
Thread 2:Second PrintQueue: Printing a Job during 4 seconds
Thread 2: The document has been printed
Thread 3:Second PrintQueue: Printing a Job during 4 seconds
Thread 3: The document has been printed
Thread 4:Second PrintQueue: Printing a Job during 1 seconds
Thread 4: The document has been printed
Thread 5:Second PrintQueue: Printing a Job during 3 seconds
Thread 5: The document has been printed
Thread 6:Second PrintQueue: Printing a Job during 0 seconds
Thread 6: The document has been printed
Thread 7:Second PrintQueue: Printing a Job during 1 seconds
Thread 7: The document has been printed
Thread 8:Second PrintQueue: Printing a Job during 5 seconds
Thread 8: The document has been printed
Thread 9:Second PrintQueue: Printing a Job during 5 seconds
Thread 9: The document has been printed
Process finished with exit code 0

從上圖能夠看出線程直接獲取鎖的順序是公平的,先到先得。

咱們運行非公平鎖的打印結果以下:

Thread 0: Going to print a job
Thread 0:First PrintQueue: Printing a Job during 5 seconds
Thread 1: Going to print a job
Thread 2: Going to print a job
Thread 3: Going to print a job
Thread 4: Going to print a job
Thread 5: Going to print a job
Thread 6: Going to print a job
Thread 7: Going to print a job
Thread 8: Going to print a job
Thread 9: Going to print a job
Thread 0:Second PrintQueue: Printing a Job during 2 seconds //線程0直接釋放鎖又獲取了鎖,體現了非公平鎖
Thread 0: The document has been printed
Thread 1:First PrintQueue: Printing a Job during 9 seconds
Thread 1:Second PrintQueue: Printing a Job during 3 seconds
Thread 1: The document has been printed
Thread 2:First PrintQueue: Printing a Job during 0 seconds
Thread 3:First PrintQueue: Printing a Job during 0 seconds
Thread 3:Second PrintQueue: Printing a Job during 7 seconds
Thread 3: The document has been printed
Thread 4:First PrintQueue: Printing a Job during 3 seconds
Thread 4:Second PrintQueue: Printing a Job during 8 seconds
Thread 4: The document has been printed
Thread 5:First PrintQueue: Printing a Job during 6 seconds
Thread 5:Second PrintQueue: Printing a Job during 1 seconds
Thread 5: The document has been printed
Thread 6:First PrintQueue: Printing a Job during 0 seconds
Thread 6:Second PrintQueue: Printing a Job during 7 seconds
Thread 6: The document has been printed
Thread 7:First PrintQueue: Printing a Job during 8 seconds
Thread 7:Second PrintQueue: Printing a Job during 1 seconds
Thread 7: The document has been printed
Thread 8:First PrintQueue: Printing a Job during 9 seconds
Thread 8:Second PrintQueue: Printing a Job during 8 seconds
Thread 8: The document has been printed
Thread 9:First PrintQueue: Printing a Job during 5 seconds
Thread 9:Second PrintQueue: Printing a Job during 5 seconds
Thread 9: The document has been printed
Thread 2:Second PrintQueue: Printing a Job during 3 seconds
Thread 2: The document has been printed
​
Process finished with exit code 0

上圖中能夠看出線程0在釋放了鎖以後,馬上有獲取了鎖繼續執行,出現了搶鎖插隊現象(此時等待隊列已經有了線程1-9再等待)。

公平鎖和非公平鎖有缺點

  • 公平鎖優點:公平鎖各個線程平等,每一個線程等待一段時間總會執行。
  • 公平鎖劣勢:相對非公平鎖執行效率比較慢,吞吐量更小
  • 非公平鎖優點:相比公平鎖更快,吞吐量更大
  • 非公平鎖劣勢:又可能產生飢餓線程,就是某些線程的等待時間很長始終得不到執行。

公平鎖和非公平鎖源碼解析
首先公平鎖和非公平鎖都是繼承了ReentrantLock類中的內部類Sync類,這個Sync類繼承AQS(AbstractQueuedSynchronizer),Sync類代碼以下:

//源碼中能夠看出Sync繼承了AbstractQueuedSynchronizer類
abstract static class Sync extends AbstractQueuedSynchronizer {...}
//Sync有公平鎖FairSync和非公平鎖NonFairSync兩個子類:
static final class NonfairSync extends Sync {。。。}
static final class FairSync extends Sync {。。}

公平鎖獲取鎖的源碼:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    //這裏和非公平鎖對比多了個!hasQueuedPredecessors()判斷
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

非公平鎖獲取鎖源碼:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

經過對比能夠發現,公平鎖和非公平鎖的區別主要是在獲取鎖的時候,
公平鎖多了一個hasQueuedPredecessors()爲false的判斷,hasQueuedPredecessors()方法
就是判斷等待隊列中是否已經有線程在等待了,若是有則當前線程不能在嘗試獲取鎖,對於非公平鎖而言,
不管是否有線程在等待了,都先嚐試獲取鎖,獲取不到的話再去排隊,tryLock()方法內部調用的是sync.nonfairTryAcquire(1)即非
公平鎖,因此即便設置了公平模式,那麼使用tryLock()也能夠插隊。

讀寫鎖

爲何設置讀寫鎖

首先讀寫鎖是爲了提升系統的效率,
雖然普通的ReentrantLock能夠保證線程安全,
可是若是是多個讀取操做,就直接採用ReentrantLock會大大的浪費系統資源,
還有就是寫操做是不安全的,當併發寫或者在進行寫操做的同時進行讀取,都會發生線程安全問題,
那麼設置的讀寫鎖就起了做用,讀寫鎖支持併發讀來提升讀的效率,同時又保證安全的寫操做。

讀寫鎖規則

  • 若是一個線程已經佔用了讀鎖,則此時其餘線程若是要申請讀鎖,能夠申請成功。
  • 若是一個線程已經佔用了讀鎖,則此時其餘線程若是要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖,由於讀寫不能同時進行。
  • 若是一個線程已經佔用了寫鎖,則此時其餘線程申請讀鎖或者寫鎖,必須等待以前的線程釋放了寫鎖,由於讀寫不能同時進行。

讀寫鎖使用展現

這裏使用ReentrantReadWriteLock來演示,ReentrantReadWriteLock是ReadWriteLock的是實現類,最主要兩個方法readLock()獲取讀鎖,writeLock()獲取寫鎖,
這裏使用ReadWriteLock中的讀寫鎖進行併發讀寫,代碼展現以下:

public class ReadWriteLock {
    //定義讀寫鎖
    private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
    //獲取讀鎖
    private static final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock();
    //獲取寫鎖
    private static final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock();
​
    public static void read(){
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() +"獲得讀鎖,正在讀取");
            Thread.sleep(500);
        }catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            System.out.println("釋放讀鎖");
            readLock.unlock();
        }
    }
​
    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "獲得寫鎖,正在寫入");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
            writeLock.unlock();
        }
    }
​
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> read()).start();
        new Thread(() -> read()).start();
        new Thread(() -> write()).start();
        new Thread(() -> write()).start();
    }
}

運行結果以下:

Thread-0獲得讀鎖,正在讀取
Thread-1獲得讀鎖,正在讀取
釋放讀鎖
釋放讀鎖
Thread-2獲得寫鎖,正在寫入
Thread-2釋放寫鎖
Thread-3獲得寫鎖,正在寫入
Thread-3釋放寫鎖

經過運行結果能夠看出,讀寫鎖支持併發讀,而寫操做是單獨進行的。

讀鎖插隊策略

首先讀寫鎖ReentrantReadWriteLock支持公平鎖和非公平鎖,能夠經過如下進行設置:

//後面的boolean值用來設置公平鎖、非公平鎖,其中的false設置爲非公平鎖,設置爲true就是公平鎖,
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);

若是設置爲公平鎖的時候對應的讀寫鎖實現爲:

static final class FairSync extends Sync {
    private static final long serialVersionUID = -2274990926593161451L;
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

其中的hasQueuedPredecessors()方法就是檢測等待隊列中是否已經有線程在排序了,
若是有的話每當前獲取鎖的線程就會block去排序,因此符合公平鎖定義。

若是設置爲false非公平鎖,則對應的實現爲:

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            /* As a heuristic to avoid indefinite writer starvation,
             * block if the thread that momentarily appears to be head
             * of queue, if one exists, is a waiting writer。  This is
             * only a probabilistic effect since a new reader will not
             * block if there is a waiting writer behind other enabled
             * readers that have not yet drained from the queue。
             */
            return apparentlyFirstQueuedIsExclusive();
        }
}

上圖中writerShouldBlock()方法直接返回false,能夠看出對於想要獲取寫鎖的線程而言,
因爲返回的是false因此它能夠隨時插隊,也符合非公平鎖的設計,非公平鎖下的獲取讀鎖須要依據apparentlyFirstQueuedIsExclusive()方法的返回值,上圖中對apparentlyFirstQueuedIsExclusive方法註釋主要是說防止等待隊
列頭的寫線程無飢餓等待下去,舉個例子說明:

場景:若是有線程1和2同時讀取,而且1和2已經持有了讀鎖,此時線程3想要寫入,因此線程3進入等待隊列,此時線程4忽然插隊想要獲取讀鎖。
此時就有兩種策略:

  • 容許插隊,容許線程4獲取讀鎖和線程1線程2一塊兒去讀取,看似提升了讀取效率,可是卻有一個嚴重的問題,就是若是後面來的線程一直都是想要獲取讀鎖的線程,那麼線程3將一直得不到執行的機會,那麼就會陷入「飢餓」狀態,在長時間內得不到執行。
  • 不容許插隊,此時若是新來的線程4想要獲取讀鎖,必須去等排隊等待,這種策略下,線程3或優先於線程4,就能夠避免上面的「飢餓」狀態,直到線程3運行結束,線程4纔有機會運行。

而ReentrantReadWriteLock非公平鎖下的獲取讀鎖正是採用了不容許插隊策略來實現的,避免了線程飢餓狀況。

咱們經過代碼展現一下上面的不容許插隊策略,效果展現代碼展現:

public class ReadWriteLock {
    //定義讀寫鎖
    private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
    //獲取讀鎖
    private static final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock();
    //獲取寫鎖
    private static final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock();
​
    public static void read(){
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() +"獲得讀鎖,正在讀取");
            Thread.sleep(500);
        }catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            System.out.println("釋放讀鎖");
            readLock.unlock();
        }
    }
​
    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "獲得寫鎖,正在寫入");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
            writeLock.unlock();
        }
    }
​
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> read()).start();
        new Thread(() -> read()).start();
        new Thread(() -> write()).start();
        //以上代碼沒有改變,這裏換成了讀鎖
        new Thread(() -> read()).start();
    }
}

運行結果以下:

Thread-0獲得讀鎖,正在讀取
Thread-1獲得讀鎖,正在讀取
釋放讀鎖
釋放讀鎖
Thread-2獲得寫鎖,正在寫入
Thread-2釋放寫鎖
Thread-3獲得讀鎖,正在讀取
釋放讀鎖

經過運行結果咱們能夠看出,ReentrantReadWriteLock選擇了不容許插隊的策略。

讀寫鎖的升降級

寫鎖的降級
寫鎖的降級,代碼展現:

//定義讀寫鎖
private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//獲取讀鎖
private static final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock();
//獲取寫鎖
private static final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock();
​
//鎖的降級
public static void downgrade(){
    System.out.println(Thread.currentThread().getName()+"嘗試獲取寫鎖");
    writeLock.lock();//獲取寫鎖
    try {
        System.out.println(Thread.currentThread().getName()+"獲取了寫鎖");
        //在不釋放寫鎖的狀況下直接獲取讀鎖,這就是讀寫鎖的降級
        readLock.lock();
        System.out.println(Thread.currentThread().getName()+"獲取了讀鎖");
    }finally {
        System.out.println(Thread.currentThread().getName()+"釋放了寫鎖");
        //釋放了寫鎖,可是依然持有讀鎖,這裏不釋放讀鎖,致使後面的線程沒法獲取寫鎖
        writeLock.unlock();
    }
}

public static void main(String[] args) {
    new Thread(() -> downgrade()).start();
    new Thread(() -> downgrade()).start();
}

上圖運行結果以下:

image.png
圖中咱們能夠看出線程0能夠在持有寫鎖的狀況下獲取到了讀鎖,這就是寫鎖的降級,由於線程0後面只是放了寫鎖,
並未釋放讀鎖,致使後面的線程1不能獲取到寫鎖,因此程序一直阻塞。

讀鎖的升級
接下來咱們在來看讀鎖的升級,代碼展現:

private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
//獲取讀鎖
private static final ReentrantReadWriteLock.ReadLock readLock= reentrantReadWriteLock.readLock();
//獲取寫鎖
private static final ReentrantReadWriteLock.WriteLock writeLock= reentrantReadWriteLock.writeLock();

//讀鎖升級
public static void upgarde(){
    System.out.println(Thread.currentThread().getName()+"嘗試獲取讀鎖");
    readLock.lock();
    try{
        System.out.println(Thread.currentThread().getName()+"獲取到了讀鎖");
        System.out.println(Thread.currentThread().getName()+"阻塞獲取寫鎖");
        //在持有讀鎖的狀況下獲取寫鎖,此處會阻塞,表示不支持讀鎖升級到寫鎖
        writeLock.lock();//此處會阻塞
        System.out.println(Thread.currentThread().getName()+"獲取到了寫鎖");
​
    }finally {
        readLock.unlock();
    }
}
​
public static void main(String[] args) {
    new Thread(() -> upgarde()).start();
    new Thread(() -> upgarde()).start();
}

運行結果以下:

image.png

上圖中咱們能夠看出線程0和線程1均可以成功的獲取到讀鎖,可是在進行鎖升級獲取寫鎖的時候都阻塞了,這是由於ReentrantReadWriteLock 不支持讀鎖升級到寫鎖。
由於讀鎖是能夠多個線程持有的,可是寫鎖只能一個線程持有,而且不可能存在讀鎖和寫鎖同時持有的狀況,也正是由於這個緣由因此升級寫鎖的過程當中,須要等待全部的讀鎖都釋放了,此時才能進行鎖升級。

舉個例子,好比ABC三個線程都持有讀鎖,其中線程A想要進行鎖升級,必需要等到B和C都釋放了讀鎖,此時線程A才能夠成功升級並獲取寫鎖。

可是這裏也有一個問題,那就是假如A和B都想要鎖升級,對於線程A來講,他須要等待其餘全部線程包括B線程釋放讀鎖,而B線程也須要等待其餘線程釋放讀鎖包括A線程,那就會發生死鎖。
因此若是咱們保證每次升級只有一個線程能夠升級,那麼就能夠保證線程安全,而且實現。

自旋鎖

自旋鎖介紹

自旋鎖其實就是指循環,好比while或者for循環,一直循環去嘗試獲取鎖,不像普通的鎖獲取不到就陷入阻塞。

自旋鎖和非自旋鎖流程圖對好比下:

image.png

上圖中咱們能夠看出自旋鎖獲取獲取鎖失敗並不會釋放CPU資源而是經過自旋的方式等待鎖的釋放,直到成功獲取到鎖爲止。
而非自旋鎖若是嘗試獲取鎖失敗了,它就把本身的線程切換狀態,讓線程休眠,釋放CPU時間片,而後直到以前持有這把鎖的線程釋放了鎖,因而CPU再把以前的線程恢復回來,讓這個線程再嘗試去獲取鎖,若是再次失敗就在讓線程休眠,若是成功,就能夠獲取到同步資源的鎖。

自旋鎖的好處
自旋鎖免去了耗時的阻塞和喚醒線程操做,避免了線程的狀態切換等開銷,提升了效率。

自旋鎖的壞處
自旋鎖雖然避免了線程切換的開銷,可是也帶來了新的開銷,由於他須要不停的循環去嘗試獲取鎖,若是因此只不釋放,那麼他就須要一直去嘗試,這樣會白白的浪費資源。

因此,自旋鎖適用於併發度不是特別高,並且線程持有鎖的時間較短的場景。舉個例子好比java.util.concurrent包下的原子類大多數都是基於自旋鎖的實現,好比AtomicInteger,咱們查看他的getAndIncrement()方法,以下:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
​
        return var5;
}

很明顯,do...while()循環就是一個自旋操做,若是在修改過程當中遇到了其餘線程致使沒有修改爲功,則就會執行循環不從的重試,直到修改爲功爲止。

如何自定義實現一個可重入的自旋鎖

實現代碼以下:

//自定義實現可重入的自旋鎖
public class CustomReentrantSpinLock {
    private AtomicReference<Thread> owner=new AtomicReference<>();
    private int count = 0;//重入次數
​
    public void lock() {
        Thread t = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"lock了");
        if (t == owner.get()) {
            ++count;
            return;
        }
​
        //自旋獲取鎖
        while (!owner.compareAndSet(null, t)) {
            System.out.println(Thread.currentThread().getName()+"自旋了");
        }
    }
​
    public void unLock(){
        Thread t=Thread.currentThread();
        //只有當前線程才能解鎖
        if(t == owner.get()){
            if(count >0){
                --count;
            } else {
                owner.set(null);
            }
        }
    }
​
    public static void main(String[] args) {
        CustomReentrantSpinLock spinLock=new CustomReentrantSpinLock();
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"開始嘗試獲取自旋鎖");
                spinLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName()+"獲取到了自旋鎖");
                    Thread.sleep(1);
                }catch (InterruptedException e){
                    e.printStackTrace();
                } finally {
                    spinLock.unLock();
                    System.out.println(Thread.currentThread().getName()+"釋放了自旋鎖");
                }
            }
        };
        Thread thread1=new Thread(runnable);
        Thread thread2=new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

運行結果以下:

Thread-0開始嘗試獲取自旋鎖
Thread-1開始嘗試獲取自旋鎖
Thread-0獲取到了自旋鎖
Thread-1自旋了
.
.
.
Thread-1自旋了
Thread-0釋放了了自旋鎖
Thread-1獲取到了自旋鎖
Thread-1釋放了了自旋鎖

從上圖運行結果中能夠看出,打印了不少Thread-1自旋了,說明自旋期間CPU依然不停運轉,Thread-1並無釋放CPU時間片。

JVM對鎖的優化

從jdk1.6後HotSpot虛擬機對synchronized作了不少優化,包括自適應自選、鎖消除、鎖粗化、偏向鎖、輕量級鎖等,使得synchronized鎖獲得了很大的性能提高。

自適應自旋鎖

自適應自旋就是自旋的時間不在固定,而是根據自旋的成功率、失敗率、以及當前鎖的持有者的狀態等多種因素來共同決定的,就是說自旋的時間是變化的,這樣能夠減小無用的自旋,提升效率。

鎖消除

鎖消除是發生在編譯器級別的一種鎖優化方式,有時候咱們寫的代碼不須要加鎖,就好比加鎖的代碼實際上只有一個線程會執行,並不會出現多個線程併發訪問的狀況,可是咱們卻加上了synchronized鎖,那麼編譯器就可能會消除掉這個鎖,好比下面StringBuffer的append操做:

@Override
public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String。valueOf(obj));
    return this;
}

代碼中咱們能夠看到這個方法被synchronized修飾,由於它可能會被多個線程同時使用,
可是多數狀況下它只會在一個線程內使用,若是編譯器能肯定這個對象只會在一個線程內使用,那麼就表示確定是線程安全的,編譯器就會作出優化,把對應的synchronized消除,省去加解鎖的操做以提高效率。

鎖粗化

鎖粗化主要是應對剛釋放鎖,什麼還沒作,就從新獲取鎖,例如以下代碼:

public void lockCoarsening() {
    synchronized (this) {
        //do something
    }
    synchronized (this) {
        //do something
    }
    synchronized (this) {
        //do something
    }
}

上述代碼中,當線程在執行第一個同步代碼塊時須要先獲取synchronized鎖,而後執行完了同步代碼塊在釋放synchronized鎖,可是當線程執行完第一個同步代碼塊後已經釋放了鎖後,緊接着線程馬上開始執行第二個同步代碼塊時就須要對相同的鎖進行獲取和釋放,
這樣釋放和獲取鎖是徹底沒有必要的,若是把同步區域擴大,也就是在最開始的時候加一次鎖,在結束的時候釋放鎖,那麼就能夠把中間無心義的解鎖和加鎖的過程消除,至關於把幾個synchronized塊合併成爲一個較大的同步塊,好處就是無需頻繁的釋放鎖和獲取鎖,減小系統開銷。

可是鎖粗化不適用在循環的場景,僅適用非循環的場景,由於以下代碼所示,若是咱們在第一次循環中獲取鎖,在最後一次循環中釋放鎖,那麼這就會致使其它線程長時間沒法獲取鎖。

for (int i = 0; i < 1000; i++) {
    synchronized (this) {
        //do something
    }
}

鎖粗化默認是開啓的,經過-XX:-EliminateLocks關閉該功能

偏向鎖、輕量級鎖、重量級鎖

這三種鎖咱們最開始就介紹過,它們是指synchronized的鎖狀態的,經過在對象頭中的mark work字段來代表鎖的狀態。

鎖升級的路徑

鎖升級的路徑以下圖所示,偏向鎖性能作好,避免了CAS操做,輕量級鎖利用了自旋和CAS操做避免了重量級鎖帶來的線程阻塞和喚醒,性能中等,重量級鎖會把獲取不到鎖的線程阻塞,性能最差。

image.png
-END
喜歡就掃描下方二維碼或微信搜索「程序員內功心法」關注我吧

在這裏插入圖片描述

相關文章
相關標籤/搜索