Java多執行緒核心技術(四)Lock的使用
本文主要介紹使用Java5中Lock物件也能實現同步的效果,而且在使用上更加方便。
本文著重掌握如下2個知識點:
- ReentrantLock 類的使用。
- ReentrantReadWriteLock 類的使用。
1. 使用ReentrantLock 類
在Java多執行緒中,可以使用 synchronized 關鍵字來實現執行緒之間同步互斥,但在JDK1.5中新增加了 ReentrantLock 類也能達到同樣的效果,並且在擴充套件功能上也更加強大,比如具有嗅探鎖定、多路分支通知等功能,而且在使用上也比 synchronized 更加的靈活。
1.1 使用ReentrantLock實現同步
呼叫ReentrantLock物件的lock()方法獲取鎖,呼叫unlock()方法釋放鎖。
下面是初步的程式示例:
public class Demo { private Lock lock = new ReentrantLock(); public void test(){ lock.lock(); for (int i= 0;i<5;i++){ System.out.println(Thread.currentThread().getName()+" - "+i); } lock.unlock(); } public static void main(String[] args) { Demo demo =new Demo(); for (int i = 0;i<5;i++){ new Thread(new Runnable() { @Override public void run() { demo.test(); } }).start(); } } }
執行結果:
Thread-0 - 0 Thread-0 - 1 Thread-0 - 2 Thread-0 - 3 Thread-0 - 4 Thread-1 - 0 Thread-1 - 1 Thread-1 - 2 Thread-1 - 3 Thread-1 - 4 Thread-2 - 0 Thread-2 - 1 Thread-2 - 2 Thread-2 - 3 Thread-2 - 4 Thread-3 - 0 Thread-3 - 1 Thread-3 - 2 Thread-3 - 3 Thread-3 - 4 Thread-4 - 0 Thread-4 - 1 Thread-4 - 2 Thread-4 - 3 Thread-4 - 4
從執行的結果來看,當前執行緒列印完畢後將鎖進行釋放,其他執行緒才可以繼續列印。
1.2 使用Condition 實現等待 / 通知
關鍵字 synchronized 與 wait() 和 notify() / notifyAll() 方法相結合可以實現等待 / 通知模式,類 ReentrantLock 也可以實現同樣的功能,但需要藉助於 Condition(即物件監視器)例項,執行緒物件可以註冊在指定的 Condition 中,從而可以有選擇性地進行執行緒通知,在排程執行緒上更加靈活。
在使用 notify() / notifyAll() 方法進行通知時,被通知的執行緒卻是由JVM隨機選擇的。但使用 ReentrantLock 結合 Condition 類是可以實現前面介紹過的“選擇性通知”,這個功能是非常重要的,而且在 Condition 類中是預設提供的。
示例程式碼:
public class Demo { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void await() { try { lock.lock(); System.out.println("開始等待:" + System.currentTimeMillis()); condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void signal() { try { lock.lock(); System.out.println("結束等待:" + System.currentTimeMillis()); condition.signal(); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Demo demo = new Demo(); new Thread(new Runnable() { @Override public void run() { demo.await(); } }).start(); Thread.sleep(3000); demo.signal(); } }
執行結果:
開始等待:1537352883839 結束等待:1537352886839
成功實現等待 / 通知模式。
在Object中,有wait() 、wait(long)、notify()、notifyAll()方法。
在Condition類中,有 await()、wait(long)、notify()、notifyAll()方法。
1.3使用多個Condition實現通知部分執行緒
示例程式碼:
public class Demo { private Lock lock = new ReentrantLock(); private Condition conditionA = lock.newCondition(); private Condition conditionB = lock.newCondition(); public void awaitA() { try { lock.lock(); System.out.println("A開始等待:" + System.currentTimeMillis()); conditionA.await(); System.out.println("A結束等待:" + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void awaitB() { try { lock.lock(); System.out.println("B開始等待:" + System.currentTimeMillis()); conditionB.await(); System.out.println("B結束等待:" + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void signalAll_B() { try { lock.lock(); conditionB.signalAll(); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Demo demo = new Demo(); new Thread(new Runnable() { @Override public void run() { demo.awaitA(); } }).start(); new Thread(new Runnable() { @Override public void run() { demo.awaitB(); } }).start(); Thread.sleep(3000); demo.signalAll_B(); } }
執行結果:
A開始等待:1537354021740 B開始等待:1537354021741 B結束等待:1537354024738
可以看到,只有B執行緒被喚醒了。
通過此實驗可知,使用 ReentrantLock 物件可以喚醒指定種類的執行緒,這是控制部分執行緒行為的方便行為。
1.4 公平鎖和非公平鎖
鎖Lock分為”公平鎖“和“非公平鎖”,公平鎖表示執行緒獲取鎖的順序是按照執行緒載入的順序來分配的,即先來先得的FIFO先進先出順序。而非公平鎖就是一種獲取鎖的搶佔機制,是隨機獲得鎖的,和公平鎖不一樣的就是先來的不一定先得到鎖,這個方式可能造成某些執行緒一直拿不到鎖,結果也就是不公平的了。
設定公平鎖:
Lock lock = new ReentrantLock(true);
使用ReentrantLock類設定公平鎖只需要在構造時傳入boolean引數即可。預設false。需要明白的是,即使設定為true也不能保證百分百公平。
1.5 方法getHoldCount()、getQueryLength()和getWaitQueryLength()
ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); lock.getHoldCount(); lock.getQueueLength(); lock.getWaitQueueLength(condition);
- 方法getHoldCount() 的作用是查詢當前執行緒保持此鎖定的個數,也就是呼叫 lock() 方法的次數。
- 方法getQueryLength() 的作用是返回正等待獲取此鎖定的執行緒估計數。比如有5個方法,1個執行緒首先執行 await()方法,那麼在呼叫getQueueLength()方法後返回值是4,說明有4個執行緒同時在等待 lock 的釋放。
- 方法getWaitQueryLength(condition) 的作用是返回等待與此鎖定相關的給定條件Condition的執行緒估計數,比如有5個執行緒,每個執行緒都執行了同一個condition 物件的await() 方法,則呼叫 getWaitQueryLength(condition) 方法時返回的int值是5。
1.6 方法hasQueuedThread()、hasQueuedThreads()和hasWaiters()
- 方法 boolean hasQueuedThread(Thread thread) 的作用是查詢指定的執行緒是否正在等待獲取此鎖定。
- 方法 boolean hasQueuedThreads() 的作用是查詢是否有執行緒正在等待獲取此鎖定。
- 方法 boolean hasWaiters(Condition condition) 的作用是查詢是否有執行緒正在等待與此鎖定有關的 condition 條件。
1.7 方法isFair()、isHeldByCurrentThread()和isLocked()
- 方法boolean isFair() 的作用是判斷是不是公平鎖。
- 方法boolean isHeldByCurrentThread() 的作用是查詢當前執行緒是否保持此鎖定。
- 方法boolean isLocked() 的作用是查詢此鎖定是否由任意執行緒保持。
更改上面的部分程式碼:
System.out.println(lock.isHeldByCurrentThread()); System.out.println(lock.isLocked()); lock.lock(); System.out.println(lock.isLocked()); System.out.println(lock.isHeldByCurrentThread());
執行結果:
false false true true
1.8 方法lockInterruptibly()、tryLock()和tryLock(long timeout, TimeUnit unit)
- 方法void lockInterruptibly()的作用是:如果當前執行緒未被中斷,則獲取鎖定,如果已經被中斷則出現異常。而使用 lock() 方法,即使執行緒被中斷(呼叫thread.interrupt()方法),也不會出現異常。
- 方法boolean tryLock() 的作用是,僅在呼叫時鎖定未被另一個執行緒保持的情況下,才獲取該鎖定。假設有兩個執行緒同時呼叫同一個lock物件的tryLock()方法,那麼除了第一個獲得鎖並返回true,其它都獲取不到鎖並返回false。
- 方法 boolean tryLock(long timeout, TimeUnit unit) 的作用是,如果鎖定在給定等待時間內沒有被另一個執行緒保持,且當前執行緒未被中斷,則獲取該鎖定。
1.9 方法 condition.awaitUninterruptibly()的使用
前面講到,執行condition.await()方法後,執行緒進入等待狀態,如果這時執行緒被中斷(呼叫thread.interrupt()方法)則會丟擲異常。而使用 condition.awaitUninterruptibly() 方法代替 condition.await() 方法則不會丟擲異常。
1.10 方法 condition.awaitUntil(Date deadline)的使用
使用方法 condition.awaitUntil(Date deadline) 可以代替 await(long time, TimeUnit unit) 方法進行執行緒等待,該方法在等待時間到達前是可以被提前喚醒的。
1.11 使用Condition實現順序執行
使用Condition物件可以對執行緒執行的業務進行排序規劃。
示例程式碼:
public class DThread{ volatile private static int nextPrintWho = 1; private static ReentrantLock lock = new ReentrantLock(); final private static Condition conditionA = lock.newCondition(); final private static Condition conditionB = lock.newCondition(); final private static Condition conditionC = lock.newCondition(); public static void main(String[] args) { Thread threadA = new Thread(){ @Override public void run() { try { lock.lock(); while (nextPrintWho != 1){ conditionA.await(); } for (int i = 0;i<3;i++){ System.out.println("ThreadA "+(i+1)); } nextPrintWho = 2; conditionB.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } }; Thread threadB = new Thread(){ @Override public void run() { try { lock.lock(); while (nextPrintWho != 2){ conditionA.await(); } for (int i = 0;i<3;i++){ System.out.println("ThreadB "+(i+1)); } nextPrintWho = 3; conditionB.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } }; Thread threadC = new Thread(){ @Override public void run() { try { lock.lock(); while (nextPrintWho != 3){ conditionA.await(); } for (int i = 0;i<3;i++){ System.out.println("ThreadC "+(i+1)); } nextPrintWho = 1; conditionB.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } }; for (int i= 0;i<5;i++){ new Thread(threadA).start(); new Thread(threadB).start(); new Thread(threadC).start(); } } }
列印結果:
ThreadA 1 ThreadA 2 ThreadA 3 ThreadB 1 ThreadB 2 ThreadB 3 ThreadC 1 ThreadC 2 ThreadC 3 ....
2.使用ReentrantReadWriteLock類
類 ReentrantLock 具有完全互斥排他的效果,即同一時間只有一個執行緒在執行ReentrantLock.lock() 方法後面的任務。這樣做雖然保證了例項變數的執行緒安全性,但效率卻是非常低下的。所以在DK中提供了一種讀寫鎖 ReentrantReadWriteLock 類,使用它可以加快執行效率,在某些不需要操作例項變數的方法中,完全可以使用讀寫 ReentrantReadWriteLock 來提升該方法的程式碼執行速度。
讀寫鎖表示也有兩個鎖,一個是讀操作相關的鎖,也稱為共享鎖 ;另一個是寫操作相關的鎖,也叫排他鎖 。也就是多個讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥。在沒有執行緒 Thread進行寫入操作時,進行讀取操作的多個 Thread 都可以獲取讀鎖,而進行寫入操作的 Thread 只有在獲取寫鎖後才能進行寫入操作。即多個 Thread可以同時進行讀取操作但是同一時刻只允許一個 Thread 進行寫入操作。
總結起來就是:讀讀共享,寫寫互斥,讀寫互斥,寫讀互斥。
宣告讀寫鎖:
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
獲取讀鎖:
lock.readLock().lock();
獲取寫鎖:
lock.writeLock().lock();
3.文末總結
學習完本文完全可以使用Lock物件將 synchronized關鍵字替換掉,而且其具有的獨特功能也是 synchronized 所不具有的。在學習併發時,Lock是synchronized關鍵字的進階,掌握Lock有助於學習併發包中原始碼的實現原理,在併發包中大量的類使用了Lock 介面作為同步的處理方式。
參考
《Java多執行緒程式設計核心技術》高洪巖著
擴充套件
ofollow,noindex" target="_blank">Java多執行緒程式設計核心技術(一)Java多執行緒技能