Thread詳解13:ReentrantLock的用法(一)

Java裏面提供了比synchronized更加靈活豐富的鎖機制,它們有一個共同的接口Lock,我們先來學習這個接口,瞭解其協議和功能。下面是JDK文檔,總結得非常精煉,包含的知識點非常多,所以一開始可能看不懂,不過沒關係,後面一點點弄懂。


public interface Lock

Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。此實現允許更靈活的結構,可以具有差別很大的屬性,可以支持多個相關的 Condition 對象。

鎖是控制多個線程對共享資源進行訪問的工具。通常,鎖提供了對共享資源的獨佔訪問。一次只能有一個線程獲得鎖,對共享資源的所有訪問都需要首先獲得鎖。不過,某些鎖可能允許對共享資源併發訪問,如 ReadWriteLock 的讀取鎖。

雖然 synchronized 方法和語句的範圍機制使得使用監視器鎖編程方便了很多,而且還幫助避免了很多涉及到鎖的常見編程錯誤,但有時也需要以更爲靈活的方式使用鎖。例如,某些遍歷併發訪問的數據結果的算法要求使用 “hand-over-hand” 或 “chain locking”:獲取節點 A 的鎖,然後再獲取節點 B 的鎖,然後釋放 A 並獲取 C,然後釋放 B 並獲取 D,依此類推。Lock 接口的實現允許鎖在不同的作用範圍內獲取和釋放,並允許以任何順序獲取和釋放多個鎖,從而支持使用這種技術。

隨着靈活性的增加,也帶來了更多的責任。不使用塊結構鎖就失去了使用 synchronized 方法和語句時會出現的鎖自動釋放功能。在大多數情況下,應該使用以下語句:

     Lock l = ...; 
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }

鎖定和取消鎖定出現在不同作用範圍中時,必須謹慎地確保保持鎖定時所執行的所有代碼用 try-finally 或 try-catch 加以保護,以確保在必要時釋放鎖。

Lock 實現提供了使用 synchronized 方法和語句所沒有的其他功能,包括提供了一個非塊結構的獲取鎖嘗試 (tryLock())、一個獲取可中斷鎖的嘗試 (lockInterruptibly()) 和一個獲取超時失效鎖的嘗試 (tryLock(long, TimeUnit))。

Lock 類還可以提供與隱式監視器鎖完全不同的行爲和語義,如保證排序、非重入用法或死鎖檢測。如果某個實現提供了這樣特殊的語義,則該實現必須對這些語義加以記錄。

注意,Lock 實例只是普通的對象,其本身可以在 synchronized 語句中作爲目標使用。獲取 Lock 實例的監視器鎖與調用該實例的任何 lock() 方法沒有特別的關係。爲了避免混淆,建議除了在其自身的實現中之外,決不要以這種方式使用 Lock 實例。除非另有說明,否則爲任何參數傳遞 null 值都將導致拋出 NullPointerException。

這裏寫圖片描述


1 使用ReentrantLock進行同步

一個可重入的互斥鎖 Lock,它具有與使用 synchronized 方法和語句所訪問的隱式監視器鎖相同的一些基本行爲和語義,但功能更強大。

ReentrantLock 將由最近成功獲得鎖,並且還沒有釋放該鎖的線程所擁有。當鎖沒有被另一個線程所擁有時,調用 lock 的線程將成功獲取該鎖並返回。如果當前線程已經擁有該鎖,此方法將立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法來檢查此情況是否發生。

建議總是 立即實踐,使用 lock 塊來調用 try,在之前/之後的構造中,最典型的代碼如下:

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() { 
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

除了實現 Lock 接口,此類還定義了 isLocked 和 getLockQueueLength 方法,以及一些相關的 protected 訪問方法,這些方法對檢測和監視可能很有用。

此鎖最多支持同一個線程發起的 2147483648 個遞歸鎖。試圖超過此限制會導致由鎖方法拋出的 Error。

首先,我們先不管它有多牛逼,我們先使用它來代替synchronized實現常規的同步,也就是串行化,然後調用其中的一些方法看一看是什麼效果:

Service.java

package testReentrantLock;

import java.util.concurrent.locks.ReentrantLock;

public class Service {
    private ReentrantLock lock = new ReentrantLock();

    public void testMethod() {
        lock.lock();
        try {
            for (int i = 0; i < 3; i++) {
                System.out.println("******  " + Thread.currentThread().getName() + " is printing " + i + "  ******");
                // 查詢當前線程保持此鎖的次數
                int holdCount = lock.getHoldCount();

                // 返回正等待獲取此鎖的線程估計數
                int queuedLength = lock.getQueueLength();

                // 如果此鎖的公平設置爲 true,則返回 true
                boolean isFair = lock.isFair();

                System.out.printf("---holdCount: %d;\n---queuedLength:%d;\n---isFair: %s\n\n", holdCount, queuedLength,
                        isFair);

                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            lock.unlock();
        }
    }

}

Thread1.java

package testReentrantLock;

public class Thread1 extends Thread {
    private Service service;

    public Thread1(Service service, String name) {
        super(name);
        this.service = service;
    }

    @Override
    public void run() {
        super.run();
        service.testMethod();
    }

    public static void main(String[] args) {
        Service service = new Service();
        Thread1 tA = new Thread1(service, "Thread-A");
        Thread1 tB = new Thread1(service, "Thread-B");
        Thread1 tC = new Thread1(service, "Thread-C");
        tA.start();
        tB.start();
        tC.start();
    }

}

輸出

******  Thread-A is printing 0  ******
---holdCount: 1;
---queuedLength:2;
---isFair: false

******  Thread-A is printing 1  ******
---holdCount: 1;
---queuedLength:2;
---isFair: false

******  Thread-A is printing 2  ******
---holdCount: 1;
---queuedLength:2;
---isFair: false

******  Thread-B is printing 0  ******
---holdCount: 1;
---queuedLength:1;
---isFair: false

******  Thread-B is printing 1  ******
---holdCount: 1;
---queuedLength:1;
---isFair: false

******  Thread-B is printing 2  ******
---holdCount: 1;
---queuedLength:1;
---isFair: false

******  Thread-C is printing 0  ******
---holdCount: 1;
---queuedLength:0;
---isFair: false

******  Thread-C is printing 1  ******
---holdCount: 1;
---queuedLength:0;
---isFair: false

******  Thread-C is printing 2  ******
---holdCount: 1;
---queuedLength:0;
---isFair: false

我稍微囉嗦地解釋一下getHoldCount,它返回的是查詢當前線程保存此lock的個數,也就是在此線程代碼內,代用lock.lock() 的次數。一般一個線程內每個需要同步的代碼塊就會使用鎖定嘛:

 lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }


2 使用Condition實現等待/通知

這裏寫圖片描述

Condition 將 Object 監視器方法(wait、notify 和 notifyAll)分解成截然不同的對象,以便通過將這些對象與任意 Lock 實現組合使用,爲每個對象提供多個等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和語句的使用,Condition 替代了 Object 監視器方法的使用。

條件(也稱爲條件隊列 或條件變量)爲線程提供了一個含義,以便在某個狀態條件現在可能爲 true 的另一個線程通知它之前,一直掛起該線程(即讓其“等待”)。因爲訪問此共享狀態信息發生在不同的線程中,所以它必須受保護,因此要將某種形式的鎖與該條件相關聯。等待提供一個條件的主要屬性是:以原子方式 釋放相關的鎖,並掛起當前線程,就像 Object.wait 做的那樣。Condition 實例實質上被綁定到一個鎖上。要爲特定 Lock 實例獲得 Condition 實例,請使用其 newCondition() 方法。

看完下面的這個例子你就會使用Condition了。

BoundedBuffer.java

package testReentrantLock;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedBuffer {
    final ReentrantLock lock = new ReentrantLock();
    // notFull 才能put
    final Condition notFull = lock.newCondition();
    // notEmpty 才能take
    final Condition notEmpty = lock.newCondition();

    final int[] items = new int[2];
    int putptr, takeptr, count;

    public void put(int x) throws InterruptedException {
        // 每次put之前線程得獲得這個鎖才行
        lock.lock();
        try {
            // 如果是full,則讓這個企圖put的線程等待
            while (count == items.length) {
                System.out.printf("----FULL---- The buffer is full!  %s has to wait.\n",
                        Thread.currentThread().getName());
                notFull.await();
            }

            // 每次只要put成功,則通知一下 notEmpty,如果存在等待take的線程,則喚醒一個讓它取
            items[putptr] = x;
            if (++putptr == items.length)
                putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public int take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                System.out.printf("----EMPTY---- The buffer is empty!  %s has to wait.\n",
                        Thread.currentThread().getName());
                notEmpty.await();
            }
            // 每次take成功,則通知 notFull,如果有等待put的線程,則讓它放
            int x = items[takeptr];
            if (++takeptr == items.length)
                takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

BufferThread.java

package testReentrantLock;

public class BufferThread extends Thread {

    private BoundedBuffer boundedBuffer = new BoundedBuffer();
    private String name;

    public BufferThread(BoundedBuffer boundedBuffer, String name) {
        super(name);
        this.boundedBuffer = boundedBuffer;
        this.name = name;
    }

    @Override
    public void run() {
        super.run();
        System.out.println(Thread.currentThread().getName() + " is running!");
        if (name.startsWith("PUT")) {
            for (int i = 1; i < 4; i++) {
                try {
                    boundedBuffer.put(i);
                    System.out.printf("--PUT-- %s has put %d into the buffer.\n", Thread.currentThread().getName(), i);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } else if (name.startsWith("TAKE")) {
            for (int i = 1; i < 4; i++) {
                try {
                    int value = boundedBuffer.take();
                    System.out.printf("--TAK-- %s has took %d from the buffer.\n", Thread.currentThread().getName(),
                            value);
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    public static void main(String[] args) {
        BoundedBuffer boundedBuffer = new BoundedBuffer();
        // 創建3個put線程,每個往Buffer裏put 3次
        BufferThread put1 = new BufferThread(boundedBuffer, "PUT1");
        BufferThread put2 = new BufferThread(boundedBuffer, "PUT2");
        BufferThread put3 = new BufferThread(boundedBuffer, "PUT3");

        // 創建2個take線程,每個從Buffer裏take 3次
        BufferThread take1 = new BufferThread(boundedBuffer, "TAKE1");
        BufferThread take2 = new BufferThread(boundedBuffer, "TAKE2");

        put1.start();
        put2.start();
        put3.start();
        take1.start();
        take2.start();
    }

}

輸出

PUT2 is running!
TAKE1 is running!
TAKE2 is running!
----EMPTY---- The buffer is empty!  TAKE2 has to wait.
PUT1 is running!
PUT3 is running!
--PUT-- PUT3 has put 1 into the buffer.
--PUT-- PUT2 has put 1 into the buffer.
--TAK-- TAKE1 has took 1 from the buffer.
--TAK-- TAKE2 has took 1 from the buffer.
--PUT-- PUT1 has put 1 into the buffer.
--TAK-- TAKE2 has took 1 from the buffer.
----EMPTY---- The buffer is empty!  TAKE1 has to wait.
--PUT-- PUT3 has put 2 into the buffer.
----FULL---- The buffer is full!  PUT2 has to wait.
--PUT-- PUT1 has put 2 into the buffer.
--PUT-- PUT2 has put 2 into the buffer.
--TAK-- TAKE1 has took 2 from the buffer.
--TAK-- TAKE2 has took 2 from the buffer.
--TAK-- TAKE1 has took 2 from the buffer.
--PUT-- PUT3 has put 3 into the buffer.
--PUT-- PUT1 has put 3 into the buffer.
----FULL---- The buffer is full!  PUT2 has to wait.

使用Condition的優越性就在於,它把等待的線程分類了,利用同一個lock創建不同的Condition,你想把等待的線程分成幾類你就創建多少個Condition就好了。在特定條件下,喚醒不同類別的等待線程,多麼方便。如果這樣說你還是不明白Condition的優越性,那麼看看同樣的功能使用synchronized編寫是怎麼樣的:

對BoundedBuffer.java的改寫:

package testReentrantLock;

public class BoundedBufferSyn {

    final int[] items = new int[2];
    int putptr, takeptr, count;

    synchronized public void put(int x) throws InterruptedException {
        // 如果是full,則讓這個企圖put的線程等待
        while (count == items.length) {
            System.out.printf("----FULL---- The buffer is full!  %s has to wait.\n", Thread.currentThread().getName());
            // 這裏的wait和Condition的await在功能上沒有什麼區別,重點在喚醒
            wait();
        }

        // 每次只要put成功,則通知一下 notEmpty,如果存在等待take的線程,則喚醒一個讓它取
        items[putptr] = x;
        if (++putptr == items.length)
            putptr = 0;
        ++count;

        // 喚醒所有等待線程,讓它們再去搶一次鎖,而無法只通知特性的線程
        notifyAll();
    }

    synchronized public int take() throws InterruptedException {
        while (count == 0) {
            System.out.printf("----EMPTY---- The buffer is empty!  %s has to wait.\n",
                    Thread.currentThread().getName());
            wait();
        }
        // 每次take成功,則通知 notFull,如果有等待put的線程,則讓它放
        int x = items[takeptr];
        if (++takeptr == items.length)
            takeptr = 0;
        --count;
        notifyAll();
        return x;
    }
}


3 公平鎖

ReentrantLock的公平鎖是個啥? 先來看看JDK文檔的解釋:


此類的構造方法接受一個可選的公平 參數。當設置爲 true 時,在多個線程的爭用下,這些鎖【傾向於】將訪問權授予等待時間最長的線程。否則此鎖將無法保證任何特定訪問順序。與採用默認設置(使用不公平鎖)相比,使用公平鎖的程序在許多線程訪問時【表現爲很低的總體吞吐量(即速度很慢,常常極其慢)】,但是在獲得鎖和保證鎖分配的均衡性時差異較小。

【不過要注意的是,公平鎖不能保證線程調度的公平性。】因此,使用公平鎖的衆多線程中的一員可能獲得多倍的成功機會,這種情況發生在其他活動線程沒有被處理並且目前並未持有鎖時。還要注意的是,未定時的 tryLock 方法並沒有使用公平設置。因爲即使其他線程正在等待,只要該鎖是可用的,此方法就可以獲得成功。


關於公平鎖,我覺得文檔已經解釋的非常清楚了,我就不編寫示例代碼了。

發佈了66 篇原創文章 · 獲贊 15 · 訪問量 24萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章