Java多線程並發07——鎖在Java中的實現


上一篇文章中,我們已經介紹過了各種鎖,讓各位對鎖有了一定的了解。接下來將為各位介紹鎖在Java中的實現。關注我的公眾號「Java面典」了解更多 Java 相關知識點。

在 Java 中主要通過使用synchronized 、 volatile關鍵字,及 Lock 接口的子類 ReentrantLock 和 ReadWriteLock 等來實現加鎖。

synchronized

屬性

synchronized 屬於獨占式的悲觀鎖,同時屬於可重入鎖。

作用

synchronized 可以把任意一個非 NULL 的對象當作鎖。其在不同場景下的作用范圍如下:

  1. 作用於方法時,鎖住的是對象的實例(this)
  2. 作用於靜態方法時,鎖住的是Class實例,會鎖住所有調用該方法的線程。(又因為Class的相關數據存儲在永久代 PermGen【Jdk1.8 則是 metaspace】,永久代是全局共享的,因此靜態方法鎖相當於類的一個全局鎖);
  3. 作用於一個對象實例時,鎖住的是所有以該對象為鎖的代碼塊。

實現

它有多個隊列,當多個線程一起訪問某個對象監視器的時候,對象監視器會將這些線程存儲在不同的容器中。

synchronized.png

  1. Wait Set:存儲調用 wait 方法被阻塞的線程;
  2. Contention List(競爭隊列):所有請求鎖的線程首先被放在這個競爭隊列中;
  3. Entry List:Contention List 中那些有資格成為候選資源的線程被移動到 Entry List 中;
  4. OnDeck:任意時刻,最多只有一個線程正在競爭鎖資源,該線程成為 OnDeck;
  5. Owner:當前已經獲取到所資源的線程被稱為 Owner;
  6. !Owner:當前釋放鎖的線程。

參考資料

volatile

屬性

比 sychronized 更輕量級的同步鎖

適用場景

使用 volatile 必須同時滿足下面兩個條件才能保證在並發環境的線程安全:

  1. 對變量的寫操作不依賴於當前值(比如 i++),或者說是單純的變量賦值(boolean flag = true);
  2. 不同的 volatile 變量之間,不能互相依賴,只有在狀態真正獨立於程序內其他內容時才能使用 volatile。

對 volatile 變量的單次讀/寫操作可以保證原子性的,如 long 和 double 類型變量,但是並不能保證 i++ 這種操作的原子性,因為本質上 i++ 是讀、寫兩次操作。

Lock

Java 中的鎖都實現於 Lock 接口,主要方法有:

  1. void lock(): 用於獲取鎖。如果鎖可用,則獲取鎖。 若鎖不可用, 將禁用當前線程,直到取到鎖;
  2. boolean tryLock():嘗試獲取鎖。如果鎖可用,則獲取鎖,並返回 true, 否則返回 false;
    該方法和lock()的區別在於,如果鎖不可用,tryLock()不會導致當前線程被禁用。
  3. tryLock(long timeout TimeUnit unit):如果鎖在給定等待時間內沒有被另一個線程保持,則獲取該鎖
  4. void unlock():釋放鎖。鎖只能由持有者釋放,如果線程並不持有鎖,卻執行該方法。可能導致異常的發生;
  5. Condition newCondition():條件對象,獲取等待通知組件。該組件和當前的鎖綁定,當前線程只有獲取了鎖,才能調用該組件的 await()方法,而調用后,當前線程將縮放鎖;
  6. getHoldCount() :查詢當前線程保持此鎖的次數
  7. getQueueLength():返回正等待獲取此鎖的線程估計數。比如啟動 10 個線程,1 個線程獲得鎖,此時返回的是 9;
  8. getWaitQueueLength:(Condition condition)返回等待與此鎖相關的給定條件的線程估計數。比如 10 個線程,用同一個 condition 對象,並且此時這 10 個線程都執行了condition 對象的 await 方法,那么此時執行此方法返回 10;
  9. hasWaiters(Condition condition):查詢是否有線程等待與此鎖有關的給定條件(condition)。對於指定 contidion 對象,有多少線程執行了 condition.await 方法;
  10. hasQueuedThread(Thread thread):查詢給定線程是否等待獲取此鎖
  11. hasQueuedThreads():是否有線程等待此鎖
  12. isFair():該鎖是否公平鎖
  13. isHeldByCurrentThread(): 當前線程是否保持鎖鎖定,線程的執行 lock 方法的前后分別是 false 和 true;
  14. isLock():此鎖是否有任意線程占用
  15. lockInterruptibly():如果當前線程未被中斷,獲取鎖

tryLock 和 lock 和 lockInterruptibly 的區別

  1. tryLock 能獲得鎖就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit unit),可以增加時間限制,如果超過該時間段還沒獲得鎖,返回 false;
  2. lock 能獲得鎖就返回 true,不能的話一直等待獲得鎖;
  3. lock 和 lockInterruptibly,如果兩個線程分別執行這兩個方法,但此時中斷這兩個線程,lock 不會拋出異常,而 lockInterruptibly 會拋出異常。

Condition

作用

Condition 的作用是對鎖進行更精確的控制。對於同一個鎖,我們可以創建多個 Condition,在不同的情況下使用不同的 Condition。

Condition 和 Object

  • 相似之處
  1. Condition 類的 awiat 方法和 Object 類的 wait 方法等效;
  2. Condition 類的 signal 方法和 Object 類的 notify 方法等效;
  3. Condition 類的 signalAll 方法和 Object 類的 notifyAll 方法等效。
  • 不同處
  1. ReentrantLock 類可以喚醒指定條件的線程,而 object 的喚醒是隨機的;
  2. Object中的 wait()、notify()、notifyAll() 方法是和 "同步鎖"(synchronized關鍵字) 捆綁使用的;而Condition是需要與 "互斥鎖"/"共享鎖" 捆綁使用的。

ReentrantLock

屬性

可重入鎖。

特點

除了能完成 synchronized 所能完成的所有工作外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等避免多線程死鎖的方法。

與 synchronized 的區別

  1. ReentrantLock 需要通過方法 lock() 與 unlock() 手動進行加鎖與解鎖操作,而 synchronized 會 被 JVM 自動加鎖、解鎖;
  2. ReentrantLock 相比 synchronized 的優勢是可中斷、公平鎖、多個鎖。
public class MyLock {

    private Lock lock = new ReentrantLock();
//    Lock lock = new ReentrantLock(true); //公平鎖
//    Lock lock = new ReentrantLock(false); //非公平鎖
    private Condition condition = lock.newCondition(); //創建 Condition

    public void testMethod() {
        try {
            lock.lock(); //lock 加鎖
            // 1:wait 方法等待:
            //System.out.println("開始 wait");
            condition.await();
            // 通過創建 Condition 對象來使線程 wait,必須先執行 lock.lock 方法獲得鎖
            // 2:signal 方法喚醒
            condition.signal(); //condition 對象的 signal 方法可以喚醒 wait 線程
            for (int i = 0; i < 5; i++) {
                System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

ReadWriteLock

屬性

共享鎖(讀-寫鎖)

特點

  1. 如果沒有寫鎖的情況下,讀是無阻塞的,在一定程度上提高了程序的執行效率;
  2. 讀寫鎖分為讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥(這是由 JVM 自己控制的,代碼只要上好相應的鎖即可)。

使用原則

  1. 如果你的代碼只讀數據,可以很多人同時讀,但不能同時寫,那就上讀鎖;
  2. 如果你的代碼修改數據,只能有一個人在寫,且不能同時讀取,那就上寫鎖。

CountDownLatch(線程計數器 )

作用

CountDownLatch 是一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。

final CountDownLatch latch = new CountDownLatch(2);
new Thread() {
    public void run() {
        System.out.println("子線程" + Thread.currentThread().getName() + "正在執行");
        Thread.sleep(3000);
        System.out.println("子線程" + Thread.currentThread().getName() + "執行完畢");
        latch.countDown();
    }

    ;
}.start();
new Thread() {
    public void run() {
        System.out.println("子線程" + Thread.currentThread().getName() + "正在執行");
        Thread.sleep(3000);
        System.out.println("子線程" + Thread.currentThread().getName() + "執行完畢");
        latch.countDown();
    }

    ;
}.start();
System.out.println("等待 2 個子線程執行完畢...");
latch.await();
System.out.println("2 個子線程已經執行完畢");
System.out.println("繼續執行主線程");

CyclicBarrierr(回環柵欄-等待至 barrier 狀態再全部同時執行)

作用

CyclicBarrier 是一個同步輔助類,允許一組線程互相等待,直到到達某個公共屏障點 (common barrier point)。因為該 barrier 在釋放等待線程后可以重用,所以稱它為循環 的 barrier。

主要方法

CyclicBarrier 中最重要的方法就是 await 方法,它有 2 個重載版本:

  1. public int await():用來掛起當前線程,直至所有線程都到達 barrier 狀態再同時執行后續任務;
  2. public int await(long timeout, TimeUnit unit):讓這些線程等待至一定的時間,如果還有線程沒有到達 barrier 狀態就直接讓到達 barrier 的線程執行后續任務。
public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier = new CyclicBarrier(N);
        for (int i = 0; i < N; i++)
            new Writer(barrier).start();
    }

    static class Writer extends Thread {
        private CyclicBarrier cyclicBarrier;

        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(5000); //以睡眠來模擬線程需要預定寫入數據操作
                System.out.println("線程" + Thread.currentThread().getName() + "寫入數據完 畢,等待其他線程寫入完畢");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println("所有線程寫入完畢,繼續處理其他任務,比如數據操作");
        }
    }

CountDownLatch 和 CyclicBarrier 的區別

  1. CountDownLatch 的作用是允許 1 或 N 個線程等待其他線程完成執行;而 CyclicBarrier 則是允許 N 個線程相互等待;
  2. CountDownLatch 的計數器無法被重置;CyclicBarrier 的計數器可以被重置后使用,因此它被稱為是循環的 barrier。

Semaphore

Semaphore 是一種基於計數的信號量。它可以設定一個閾值,基於此,多個線程競爭獲取許可信號,做完自己的申請后歸還,超過閾值后,線程申請許可信號將會被阻塞。Semaphore 可以用來構建一些對象池,資源池之類的,比如數據庫連接池。

實現互斥鎖(計數器為 1)

我們也可以創建計數為 1 的 Semaphore,將其作為一種類似互斥鎖的機制,這也叫二元信號量,表示兩種互斥狀態。

代碼實現

它的用法如下:

// 創建一個計數閾值為 5 的信號量對象
// 只能 5 個線程同時訪問
Semaphore semp = new Semaphore(5);
try { // 申請許可
    semp.acquire();
    try {
        // 業務邏輯
    } catch (Exception e) {
    } finally {
        // 釋放許可
        semp.release();
    }
} catch (InterruptedException e) {
}

Semaphore 與 ReentrantLock 相似處

Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也有許多類似之處:

  1. 都需要手動加鎖。通過 acquire() 與 release() 方法來獲得和釋放資源;
  2. Semaphone.acquire()方法默認為可響應中斷鎖,與 ReentrantLock.lockInterruptibly() 作用效果一致,也就是說在等待臨界資源的過程中可以被 Thread.interrupt() 方法中斷;
  3. Semaphore 也實現了可輪詢的鎖請求與定時鎖的功能,除了方法名 tryAcquire 與 tryLock不同,其使用方法與 ReentrantLock 幾乎一致;
  4. Semaphore 也提供了公平與非公平鎖的機制,也可在構造函數中進行設定;
  5. 鎖釋放方式相同。與 ReentrantLock 一樣 Semaphore 的鎖釋放操作也由手動進行。同時,為避免線程因拋出異常而無法正常釋放鎖的情況發生,釋放鎖的操作也必須在 finally 代碼塊中完成。

多線程與並發系列推薦

Java多線程並發06——CAS與AQS

Java多線程並發05——那么多的鎖你都了解了嗎

Java多線程並發04——合理使用線程池

Java多線程並發03——什么是線程上下文,線程是如何調度的

Java多線程並發02——線程的生命周期與常用方法,你都掌握了嗎

Java多線程並發01——線程的創建與終止,你會幾種方式


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM