詳解JUC並發編程之鎖

當多個線程訪問一個對象時,如果不用考慮這些線程在運行環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行為都可以獲得正確的結果,那麼這個對象就是線程安全的。但是現實並不是這樣子的,所以JVM實現瞭鎖機制,今天就叭叭叭JAVA中各種各樣的鎖。

1、自旋鎖和自適應鎖

自旋鎖:在多線程競爭的狀態下共享數據的鎖定狀態隻會持續很短的一段時間,為瞭這段時間去掛起和恢復阻塞線程並不值得,而是讓沒有獲取到鎖的線程自旋(自旋並不會放棄CPU的分片時間)等待當前線程釋放鎖,如果自旋超過瞭限定的次數仍然沒有成功獲取到鎖,就應該使用傳統的方式去掛起線程瞭,在JDK定義中,自旋鎖默認的自旋次數為10次,用戶可以使用參數-XX:PreBlockSpin來更改(jdk1.6之後默認開啟自旋鎖)。

自適應鎖:為瞭解決某些特殊情況,如果自旋剛結束,線程就釋放瞭鎖,那麼是不是有點不劃算。自適應自旋鎖是jdk1.6引入,規定自旋的時間不再固定瞭,而是由前一次在同一個鎖上的自旋 時間及鎖的擁有者的狀態來決定的。如果在同一個鎖對象上,自旋等待剛剛成功獲取過鎖,並且持有鎖的線程正在運行中,那麼JVM會認為該線程自旋獲取到鎖的可能性很大,會自動增加等待時間。反之就認為不容易獲取到鎖,而放棄自旋這種方式。

鎖消除:鎖消除時指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。意思就是:在一段代碼中,堆上的所有數據都不會逃逸出去而被其他線程訪問到那就可以把他們當作棧上的數據對待,認為他們是線程私有的,不用再加鎖。

鎖粗化:

  public static void main(String[] args) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("a");
        buffer.append("b");
        buffer.append("c");
        System.out.println("拼接之後的結果是:>>>>>>>>>>>"+buffer);
    }
  @Override
    @IntrinsicCandidate
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

StringBuffer 在拼接字符串時是同步的。但是在一系列的操作中都對同一個對象(StringBuffer )反復加鎖和解鎖,頻繁的進行加鎖解鎖操作會導致不必要的性能損耗,JVM會將加鎖同步的范圍擴展到整個操作的外部,隻加一次鎖。

2、輕量級鎖和重量級鎖

這種鎖實現的背後基於這樣一種假設,即在真實的情況下我們程序中的大部分同步代碼一般都處於無鎖競爭狀態(即單線程執行環境),在無鎖競爭的情況下完全可以避免調用操作系統層面的重量級互斥鎖, 取而代之的是在monitorenter和monitorexit中隻需要依靠一條CAS原子指令就可以完成鎖的獲取及釋放。輕量級鎖是相對於重量級鎖而言的。

輕量級鎖加鎖過程

在HotSpot虛擬機的對象頭分為兩部分,一部分用於存儲對象自身的運行時數據,如Hashcode、GC分代年齡、標志位等,這部分長度在32位和64位的虛擬機中分別是32bit和64bit,稱為Mark Word。另一部分用於存儲指向方法區對象類型數據的指針,如果是數組對象的話,還會有一個額外的部分用於存儲數組長度。

對象頭信息是與對象自身定義的數據無關的額外存儲成本,Mark Word 被設計成一個非固定的數據結構以便在極小的空間內存儲盡量多的信息。mark word中有兩個bit存儲鎖標記位。

HotSpot虛擬機對象頭Mark Word

存儲內容 標志位 狀態
對象哈希碼,分代年齡 01 無鎖
指向鎖記錄的指針 00 輕量級鎖
指向重量級鎖的指針 10 膨脹重量級鎖
空,不需要記錄信息 11 GC標記
偏向線程id,偏向時間戳,對象分代年齡 01 可偏向

在代碼進入同步代碼塊時,如果此對象沒有被鎖定(標記位為01狀態),虛擬機首先在當前線程的棧幀建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象目前Mark Word的拷貝,然後虛擬機使用CAS操作嘗試將對象的Mark Word 更新為指向Lock Record的指針,如果操作成功瞭,那麼這個線程就有瞭這個對象的鎖,並且將Mark Word 的標記位更改為00,表示這個對象處於輕量級鎖定狀態。如果更新失敗瞭虛擬機會首先檢查是否是當前線程擁有瞭這個對象的鎖,如果是就進入同步代碼,如果不是,那就說明鎖被其他線程占用瞭。如果有兩個以上的線程爭奪同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖,鎖標記位變為10,後面等待的線程就要進入阻塞狀態。

輕量級鎖解鎖過程

解鎖過程同樣使用CAS操作來進行,使用CAS操作將Mark Word 指向Lock Record 指針釋放,如果操作成功,那麼整個同步過程就完成瞭,如果釋放失敗,說明有其他線程嘗試獲取該鎖,那就在釋放鎖的同時,喚醒被掛起的線程。

3、偏向鎖

JVM 參數 -XX:-UseBiasedLocking 禁用偏向鎖;-XX:+UseBiasedLocking 啟用偏向鎖。

        啟用瞭偏向鎖才會執行偏向鎖的操作。當鎖對象第一次被線程獲取時,虛擬機會把對象頭中的標記位設置為01,偏向模式。同時使用CAS操作獲取到當前線程的線程ID存儲到Mark Word 中,如果操作成功,那麼持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,都不需要任何操作,直接進入。如果有多個線程去嘗試獲取這個鎖時,偏向鎖就宣告無效,然後會撤銷偏向或者恢復到未鎖定。然後再膨脹為重量級鎖,標記位狀態變為10。

4、可重入鎖和不可重入鎖

可重入鎖就是一個線程獲取到鎖之後,在另一個代碼塊還需要該鎖,那麼不需要重新獲取而可以直接使用該鎖。大多數的鎖都是可重入鎖。但是CAS自旋鎖不可重入。

package com.xiaojie.juc.thread.lock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * @author xiaojie
 * @version 1.0
 * @description: 測試鎖的重入性
 * @date 2021/12/30 22:09
 */
public class Test01 {
    public synchronized void a() {
        System.out.println(Thread.currentThread().getName() + "運行a方法");
        b();
    }
    private synchronized void b() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "運行b方法");
    }
    public static void main(String[] args) {
        Test01 test01 = new Test01();
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i=0;i<10;i++){
            executorService.execute(() -> test01.a());
        }
    }
}

5、悲觀鎖和樂觀鎖

悲觀鎖總是悲觀的,總是認為會發生安全問題,所以每次操作都會加鎖。比如獨占鎖、傳統數據庫中的行鎖、表鎖、讀鎖、寫鎖等。悲觀鎖存在以下幾個缺點:

  • 在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延遲,引起性能問題。
  • 一個線程占有鎖後,其他線程就得阻塞等待。
  • 如果優先級高的線程等待一個優先級低的線程,會導致線程優先級導致,可能引發性能風險。

樂觀鎖總是樂觀的,總是認為不會發生安全問題。在數據庫中可以使用版本號實現樂觀鎖,JAVA中的CAS和一些原子類都是樂觀鎖的思想。

6、公平鎖和非公平鎖

公平鎖:是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。

非公平鎖:非公平鎖不需要按照申請鎖的時間順序來獲取鎖,而是誰能獲取到CPU的時間片誰就先執行。非公平鎖的優點是吞吐量比公平鎖大,缺點是有可能導致線程優先級反轉或者造成過線程饑餓現象(就是有的線程玩命的一直在執行任務,有的線程至死沒有執行一個任務)。

synchronized中的鎖是非公平鎖,ReentrantLock默認也是非公平鎖,但是可以通過構造函數設置為公平鎖。

7、共享鎖和獨占鎖

共享鎖就是同一時刻允許多個線程持有的鎖。例如Semaphore(信號量)、ReentrantReadWriteLock的讀鎖、CountDownLatch倒數閂等。

獨占鎖也叫排它鎖、互斥鎖、獨占鎖是指鎖在同一時刻隻能被一個線程所持有。例如synchronized內置鎖和ReentrantLock顯示鎖,ReentrantReadWriteLock的寫鎖都是獨占鎖。

package com.xiaojie.juc.thread.lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
 * @description: 讀寫鎖驗證共享鎖和獨占鎖
 * @author xiaojie
 * @date 2021/12/30 23:28
 * @version 1.0
 */
public class ReadAndWrite {
    static class ReadThred extends Thread {
        private ReentrantReadWriteLock lock;
        private String name;
        public ReadThred(String name, ReentrantReadWriteLock lock) {
            super(name);
            this.lock = lock;
        }
        @Override
        public void run() {
            try {
                lock.readLock().lock();
                System.out.println(Thread.currentThread().getName() + "這是共享鎖。。。。。。");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.readLock().unlock();
                System.out.println(Thread.currentThread().getName() + "釋放鎖成功。。。。。。");
            }
        }
    }
    static class WriteThred extends Thread {
        private ReentrantReadWriteLock lock;
        private String name;
        public WriteThred(String name, ReentrantReadWriteLock lock) {
            super(name);
            this.lock = lock;
        }
        @Override
        public void run() {
            try {
                lock.writeLock().lock();
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName() + "這是獨占鎖。。。。。。。。");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.writeLock().unlock();
                System.out.println(Thread.currentThread().getName() + "釋放鎖。。。。。。。");
            }
        }
    }
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReadThred readThred1 = new ReadThred("read-thread-1", reentrantReadWriteLock);
        ReadThred readThred2 = new ReadThred("read-thread-1", reentrantReadWriteLock);
        WriteThred writeThred1 = new WriteThred("write-thread-1", reentrantReadWriteLock);
        WriteThred writeThred2 = new WriteThred("write-thread-2", reentrantReadWriteLock);
        readThred1.start();
        readThred2.start();
        writeThred1.start();
        writeThred2.start();
    }
}

8、可中斷鎖和不可中斷鎖

可中斷鎖隻在搶占鎖的過程中可以被中斷的鎖如ReentrantLock。

不可中斷鎖是不可中斷的鎖如java內置鎖synchronized。

總結:

名稱

優點

缺點

使用場景

偏向鎖

加鎖和解鎖不需要CAS操作,沒有額外的性能消耗,和執行非同步方法相比僅存在納秒級的差距

如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗

適用於隻有一個線程訪問同步快的場景

輕量級鎖

競爭的線程不會阻塞,提高瞭響應速度

如線程成始終得不到鎖競爭的線程,使用自旋會消耗CPU性能

追求響應時間,同步快執行速度非常快

重量級鎖

線程競爭不適用自旋,不會消耗CPU

線程阻塞,響應時間緩慢,在多線程下,頻繁的獲取釋放鎖,會帶來巨大的性能消耗

追求吞吐量,同步快執行速度較長

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: