多執行緒_鎖

小猴子_X發表於2022-02-09

介紹鎖之前,先介紹一下JUC(java util concurrent)。它是java提供的一個工具包,裡面有我們常用的各種鎖,它分為3個包

  • java.util.concurrent                //如:volatile,CountDownLatch,CyclicBarrier,Semaphore
  • java.util.concurrent.atomic    //原子操作類物件:AtomicInteger...
  • java.util.concurrent.locks      //鎖:ReentrantLock,ReentrantReadWriteLock...
一:公平鎖/非公平鎖
  • 公平鎖:加鎖前檢查是否有排隊等待的執行緒,優先排隊等待的執行緒,先來先得
  • 非公平鎖:加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待。
    • 非公平鎖效能比公平鎖高 5~10 倍,因為公平鎖需要在多核的情況下維護一個佇列 
    • ReentrantLock 預設的 lock()方法採用的是非公平鎖,可以通過new ReentrantLock(true)設定為公平鎖
二.可重入鎖(遞迴鎖)
  • 指一個執行緒獲取外層函式鎖之後,內層遞迴函式也能仍然獲得該鎖的程式碼 (就像進入自己的家的防盜門後,也同樣可以進臥室,衛生間...)
  • ReentrantLock和synchronized 都是可重入鎖(遞迴鎖)   

比如:

    public synchronized void method1(){
        ...
        method2();
    }
    public synchronized void method2(){
        ...
    }

作用:可重入鎖最大的作用就是避免死鎖

三.自旋鎖
  • 指嘗試獲取鎖的執行緒不會立即阻塞,而是採用迴圈的方式去嘗試獲取鎖。
  • 這樣的好處是減少執行緒上下文切換的消耗(因為執行緒阻塞/喚醒代價很大),缺點是迴圈會消耗CPU。
  • 應用:原子性操作類AtomicXXX就是採用自旋鎖+CAS使用。
手寫一個自旋鎖
public class SpinLockDemo {
    // 原子引用執行緒, 沒寫引數,引用型別預設為null
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    //上鎖
    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "==>mylock");
    // 自旋
        while (!atomicReference.compareAndSet(null, thread)) {
        }
    //執行業務程式碼 ...
        System.out.println(Thread.currentThread().getName() + "開始執行業務程式碼");
    }

    //解鎖
    public void myUnlock() {
        //執行業務程式碼 ...
        System.out.println(Thread.currentThread().getName() + "結束執行業務程式碼");
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "==>myUnlock");
    }

    // 測試
    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnlock();
        }, "T1").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnlock();
        }, "T2").start();
    }
}
 四.讀寫鎖
  • 讀鎖(共享鎖):該鎖可被多個執行緒所持有。
  • 寫鎖(獨佔鎖): 指該鎖一次只能被一個執行緒鎖持有
    • 對於ReentranrLock和Synchronized而言都是獨佔鎖。

 疑問:讀鎖和不加鎖有啥區別?

  • 讀寫鎖是互斥的,共享的讀鎖是為了鎖住寫執行緒,也就是說在讀的時候不能寫 。 

好處:

  • 讀寫鎖保證:
    • 讀-讀:能共存
    • 讀-寫:不能共存
    • 寫-寫:不能共存  
  • 提高併發的效率 

使用

 ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    //讀鎖
    readWriteLock.readLock().lock();
    readWriteLock.readLock().unlock();
    //寫鎖
    readWriteLock.writeLock().lock();
    readWriteLock.writeLock().unlock(); 
 五.悲觀鎖和樂觀鎖?
  • 悲觀鎖:很悲觀,每次操作都加鎖。比如sync和lock.
  • 樂觀鎖:很樂觀,每次都不加鎖。但是更新時會判斷一個條件。
    • 一般採用version機制,和cas機制。
    • update t_goods set status=2,version=version+1 where id=#{id} and version=#{version};
六.互斥鎖/同步鎖
  • 互斥鎖:互斥是通過競爭對資源的獨佔使用,彼此沒有什麼關係,也沒有固定的執行順序。
    • 就像加sync,lock鎖的時候就是互斥鎖。
  • 同步鎖:同步是執行緒通過一定的邏輯順序佔有資源,有一定的合作關係去完成任務。
    • 就像Barrier,Semphore這樣的機制。執行完某些執行緒才能執行下一個執行緒。
七.synchronized

synchronized 和 lock 區別: 最關鍵的就是 lock定製化比較高

  1. 首先synchronized是java內建關鍵字,在jvm層面,Lock是個java類;
  2. synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖; 
  3. synchronized會自動釋放鎖(a 執行緒執行完同步程式碼會釋放鎖 ;b 執行緒執行過程中發生異常會釋放 鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成執行緒死鎖; 
  4. 用synchronized關鍵字的兩個執行緒1和執行緒2,如果當前執行緒1獲得鎖,執行緒2執行緒等待。如果執行緒1 阻塞,執行緒2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,執行緒可以不用一直等待就結束了;
  5. synchronized的鎖可重入、不可中斷、非公平;而Lock鎖可重入、可判斷、可公平(兩者皆可)
  6. lock鎖可以喚醒指定執行緒

 

sync鎖升級
JDK1.6之後,對Synchronized進行了升級,將鎖分為幾個狀態: 無鎖->偏向鎖->輕量級鎖->重量級鎖 (不可逆)。
  • 無鎖:沒有對資源進行鎖定,所有的執行緒都能訪問並修改同一個資源
  • 偏向鎖:偏向第一個訪問Thread,非常的樂觀,從始至終只有一個執行緒請求某一把鎖
    • 加鎖:
      • 當執行緒第一次訪問時,在物件頭的相關位置(鎖狀態持有的鎖偏向鎖id記錄threadID)進行記錄。
      • 後來這個執行緒再次進入時,比對ThreadID進行操作。
    • 解鎖:
      • 偏向鎖使用了一種等待競爭出現才釋放鎖的機制,只有別的執行緒也訪問該資源失敗時, 升級為輕量級鎖。
      • 過程:在全域性安全點,暫停擁有偏向鎖的執行緒,判斷偏向鎖執行緒是否存活,存活就升級為輕量級鎖,不存活就先變為無鎖狀態,再把搶佔的執行緒變為偏向鎖。
  • 輕量級鎖:多個執行緒在不同的時間段請求同一把鎖,也就是說沒有鎖競爭
    • 加鎖:
      • 在訪問執行緒的棧中建立一個鎖記錄空間,將物件頭的資訊放在這個空間,然後使用CAS將 物件頭轉變為一種指標,指向鎖記錄空間
    • 解鎖:
      • 使用CAS將鎖記錄空間的資訊替換到物件頭中。
      • 當存在多個執行緒競爭鎖的時候,就會進行自旋,自旋到一定數量,轉變為重量級鎖
  • 重量級鎖:當一個執行緒拿到鎖時候,其他執行緒會進入阻塞狀態,當鎖被釋放的時候喚醒其他執行緒  

sync的其他優化:

  • 鎖粗化:將多次連線在一起的加鎖、解鎖操作合併為一次,將多個連續的鎖擴充套件成為一個範圍更大的鎖。 比如stringBuffer.append方法.
  • 鎖消除:根據逃逸技術,如果認為這段程式碼是執行緒安全的,刪除沒有必要的加鎖操作

 

synchronized 的實現原理:

  • 同步程式碼塊是通過monitorEntermonitorExit指令(位元組碼指令)獲取執行緒的執行權。
  • 同步方法是通過acc_synchronized標誌實現執行緒的執行權的控制,一旦執行到同步方法, 就會先判斷是否有標誌位,才會去隱式的呼叫上面兩個指令。
  • 具體過程:monitor物件存在於每個物件的物件頭,進入一個同步程式碼塊,就會執行monitorEnter,就會獲取當前物件的一個持有權,這個時候monitor的計數器為1,當前執行緒就是這個monitor 的持有者,如果你已經是這個monitor的owner,再次進入,monitor就會 +1,同理當他執行 完monitorExit,對應的計數器就會減一,直到計數器為0,就會釋放持有權。
八.JUC的工具類(同步鎖)
  • CountDownLatch:執行緒計數器(遞減)
   //定義指定數量一個執行緒計數器
    CountDownLatch count = new CountDownLatch(6);
    //計數器-1
    count.countDown();
    //執行緒阻塞,等待計數器歸零
    count.await();
  • CyclicBarrier:柵欄(七顆龍珠召喚神龍)(遞加)
   //定義一個類似"召喚神龍"的物件,所有執行緒執行完再執行該執行緒
    CyclicBarrier cyclicBarrier = new CyclicBarrier(8,()->{});
    //所有執行緒進入等待,等待執行緒全部執行完
    cyclicBarrier.await();

小結:上述兩個方法雖然都是為了等待前面所有的執行緒執行完再執行後續的執行緒 ,但是CountDownLatc的後續執行緒只能是主執行緒,不能是分執行緒; 而CyclicBarrier的後續執行緒可以是分執行緒(自定義一個執行緒)

  • Semaphore:訊號量 (類似於阻塞佇列)
    • 用於併發執行緒數的控制
(比如固定的幾個停車位,每個執行緒就是一個車,搶停車位)
    //定義一個停車位的物件
    Semaphore semaphore = new Semaphore(8);
    //執行緒拿到停車位
    semaphore.acquire();
    //執行緒釋放停車位
    semaphore.release();

 

 

寄語:做顆星星,有稜有角,還會發光

相關文章