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

上一篇文章中,咱們已經介紹過了各類鎖,讓各位對鎖有了必定的瞭解。接下來將爲各位介紹鎖在Java中的實現。關注個人公衆號「Java面典」瞭解更多 Java 相關知識點。html

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

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 更輕量級的同步鎖ide

適用場景

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

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

對 volatile 變量的單次讀/寫操做能夠保證原子性的,如 long 和 double 類型變量,可是並不能保證 i++ 這種操做的原子性,由於本質上 i++ 是讀、寫兩次操做。ui

Lock

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

  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——線程的建立與終止,你會幾種方式

相關文章
相關標籤/搜索