目前對於同步,僅僅介紹了一個關鍵字synchronized,可以用於保證線程同步的原子性、可見性、有序性
對於synchronized關鍵字,對於靜態方法默認是以該類的class對象作為鎖,對於實例方法默認是當前對象this,對於同步代碼塊,需要指定鎖對象
對於整個同步方法或者代碼塊,不再需要顯式的進行加鎖,默認這一整個范圍都是在鎖范圍內
可以理解為,隱含的在代碼開始和結尾處,進行了隱式的加鎖和解鎖
所以synchronized又被稱為隱式鎖
對於synchronized關鍵字的隱式鎖,不需要顯式的加鎖和釋放,即使出現了問題,仍舊能夠對鎖進行釋放
synchronized是一種阻塞式的,在前面也提到過,對於synchronized修飾的同步,如果無法進入監視器則是BLOCKED狀態,無疑,性能方面可想而知
而且,這種隱式鎖,在同一個代碼片段內只有一個監視器,靈活性不夠
為了優化synchronized的一些不便,Java又提出來了顯式鎖的概念Lock
顧名思義,顯式,是相對隱式來說的,也就是對於加鎖和解鎖,需要明確的給出,而不會自動的進行處理
示例回顧
回憶下是之前《多線程協作wait、notify、notifyAll方法簡介理解使用 》一文中使用的例子
ps:下面的例子是優化過的,其中if判斷換成了while 循環檢測,notify換成了notifyAll
package test1; import java.util.LinkedList; /** * 消息隊列MessageQueue 測試 */ public class T14 { public static void main(String[] args) { final RefactorMessageQueue mq = new RefactorMessageQueue(5); System.out.println("***************task begin***************"); //創建生產者線程並啟動 for (int i = 0; i < 20; i++) { new Thread(() -> { while (true) { mq.set(new Message()); } }, "producer"+i).start(); } //創建消費者線程並啟動 new Thread(() -> { while (true) { mq.get(); } }, "consumer").start(); } } /** * 消息隊列 */ class RefactorMessageQueue { /** * 隊列最大值 */ private final int max; /* * 鎖 * */ private final byte[] lock = new byte[1]; /** * final確保發布安全 */ final LinkedList<Message> messageQueue = new LinkedList<>(); /** * 構造函數默認隊列大小為10 */ public RefactorMessageQueue() { max = 10; } /** * 構造函數設置隊列大小 */ public RefactorMessageQueue(int x) { max = x; } public void set(Message message) { synchronized (lock) { //如果已經大於隊列個數,隊列滿,進入等待 while (messageQueue.size() > max) { try { System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting..."); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果隊列未滿,生產消息,隨后通知lock上的等待線程 //每一次的消息生產,都會通知消費者 System.out.println(Thread.currentThread().getName() + " : add a message"); messageQueue.addLast(message); lock.notifyAll(); } } public void get() { synchronized (lock) { //如果隊列為空,進入等待,無法獲取消息 while (messageQueue.isEmpty()) { try { System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting..."); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //隊列非空時,讀取消息,隨后通知lock上的等待線程 //每一次的消息讀取,都會通知生產者 System.out.println(Thread.currentThread().getName() + " : get a message"); messageQueue.removeFirst(); lock.notifyAll(); } } }
分析下這個示例中的一些概念
使用了synchronized用作同步,鎖對象為 private final byte[] lock = new byte[1];
有多個生產者和一個消費者,為了進行通信使用了監視器(也就是鎖對象)的wait和notifyAll方法進行通信
ps:前文也說過為何要用notifyAll而不是notify
簡單說兩個點:
- synchronized關鍵字
- 監視器方法
借助於這兩個點,可以完成多線程之間的協作與通信(多個生產者一個消費者)
監視器方法的調用需要在監視器內,也就是同步方法內
而且上面的例子中的監視器都是同一個就是鎖對象,wait是當前線程在監視器上wait,notifyAll方法則是喚醒所有在此監視器上等待的線程
很顯然,其實生產者應該喚醒生產者,消費者應該喚醒消費者
可是,多線程協作使用的是同一個隊列,所以需要使用同一把鎖
又因為監視器方法必須在同步方法內而且也必須是持有監視器才能調用相應的監視器方法,所以只能使用同一個監視器了
也就是只能將這些線程組織在同一個監視器中,就不好做到“其實生產者應該喚醒生產者,消費者應該喚醒消費者”
顯式鎖邏輯
再回過頭看顯式鎖,他是如何做到各方面靈活的呢?
從上面的分析來看主要就是因為隱式鎖與監視器之間的比較強的關聯關系
synchronized修飾的代碼片段使用的是同一把鎖,同步方法內的監視器方法也只能調用這個鎖的,也就是說在使用上來看,用什么鎖,就要用這個鎖的監視器,強關聯
問題的一種解題思路就是解耦,顯式鎖就是這種思路
Lock就好比是synchronized關鍵字,只不過你需要顯式的進行加鎖和解鎖
慣用套路如下
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
本來使用synchronized隱式的加鎖和解鎖,換成了Lock的lock和unlock方法調用
那么監視器呢?
與鎖關聯的監視器又是什么,又如何調用監視器的方法呢?
Lock提供了Condition newCondition();方法
返回類型為Condition,被稱之為條件變量,可以認為是鎖關聯的監視器
借助於Condition,就可以達到原來監視器方法調用的效果,Condition方法列表如下,看得出來,是不是很像wait和notify、notifyAll?目標是一致的
所以可以說,顯式鎖的邏輯就是借助於Lock接口以及Condition接口,實現了對synchronized關鍵字以及鎖對應的監視器的另外的一種實現
從而提供了更大的靈活性
還是之前的示例,嘗試試用一下顯式鎖
package test2; import java.util.LinkedList; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class T26 { public static void main(String[] args) { final RefactorMessageQueue mq = new RefactorMessageQueue(5); System.out.println("***************task begin***************"); //創建生產者線程並啟動 for (int i = 0; i < 20; i++) { new Thread(() -> { while (true) { mq.set(new Message()); } }, "producer" + i).start(); } //創建消費者線程並啟動 new Thread(() -> { while (true) { mq.get(); } }, "consumer").start(); } /** * 消息隊列中存儲的消息 */ static class Message { } /** * 消息隊列 */ static class RefactorMessageQueue { /** * 隊列最大值 */ private final int max; /* * 鎖 * */ private final Lock lock = new ReentrantLock(); /** * 條件變量 */ private final Condition condition = lock.newCondition(); /** * final確保發布安全 */ final LinkedList<Message> messageQueue = new LinkedList<>(); /** * 構造函數默認隊列大小為10 */ public RefactorMessageQueue() { max = 10; } /** * 構造函數設置隊列大小 */ public RefactorMessageQueue(int x) { max = x; } public void set(Message message) { lock.lock(); try { //如果已經大於隊列個數,隊列滿,進入等待 while (messageQueue.size() > max) { try { System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting..."); condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果隊列未滿,生產消息,隨后通知lock上的等待線程 //每一次的消息生產,都會通知消費者 System.out.println(Thread.currentThread().getName() + " : add a message"); messageQueue.addLast(message); condition.signalAll(); } finally { } lock.unlock(); } public void get() { lock.lock(); try { //如果隊列為空,進入等待,無法獲取消息 while (messageQueue.isEmpty()) { try { System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting..."); condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } //隊列非空時,讀取消息,隨后通知lock上的等待線程 //每一次的消息讀取,都會通知生產者 System.out.println(Thread.currentThread().getName() + " : get a message"); messageQueue.removeFirst(); condition.signalAll(); } finally { lock.unlock(); } } } }
改變的核心邏輯就是鎖和條件變量
/* * 鎖 * */ private final Lock lock = new ReentrantLock(); /** * 條件變量 */ private final Condition condition = lock.newCondition();
- 使用lock.lock();以及lock.unlock(); 替代了synchronized(lock)
- 使用condition的await和signalAll方法替代了lock.wait()和 lock.notifyAll
看起來與使用synchronized關鍵字好像差不多,這沒什么毛病
顯式鎖的設計本來就是為了彌補隱式鎖的,雖說不是說作為一種替代品,但是功能邏輯的相似性是必然的
注意到,使用條件變量,與隱式鎖中都是只有一個監視器,所有的線程仍舊都是被喚醒
前面提到過,其實生產者應該喚醒消費者,消費者才應該喚醒生產者
是不是可以兩個變量?
對於生產者來說,只要非滿即可,如果滿了等待,非滿生產然后喚醒消費者
對於消費者來說,只要非空即可,如果空了等待,非空消費然后喚醒生產者
可以定義兩個條件變量,如下所示完整代碼
其實只是定義了兩個監視器
package test2; import java.util.LinkedList; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class T27 { public static void main(String[] args) { final RefactorMessageQueue mq = new RefactorMessageQueue(5); System.out.println("***************task begin***************"); //創建生產者線程並啟動 for (int i = 0; i < 20; i++) { new Thread(() -> { while (true) { mq.set(new Message()); } }, "producer" + i).start(); } //創建消費者線程並啟動 new Thread(() -> { while (true) { mq.get(); } }, "consumer").start(); } /** * 消息隊列中存儲的消息 */ static class Message { } /** * 消息隊列 */ static class RefactorMessageQueue { /** * 隊列最大值 */ private final int max; /* * 鎖 * */ private final Lock lock = new ReentrantLock(); /** * 條件變量,用於消費者,非空即可消費 */ private final Condition notEmptyCondition = lock.newCondition(); /** * 條件變量,用於生產者,非滿即可生產 */ private final Condition notFullCondition = lock.newCondition(); /** * final確保發布安全 */ final LinkedList<Message> messageQueue = new LinkedList<>(); /** * 構造函數默認隊列大小為10 */ public RefactorMessageQueue() { max = 10; } /** * 構造函數設置隊列大小 */ public RefactorMessageQueue(int x) { max = x; } public void set(Message message) { lock.lock(); try { //如果已經大於隊列個數,隊列滿,進入等待 while (messageQueue.size() > max) { try { System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting..."); //如果滿了,生產者在“非滿”這個條件上等待 notFullCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果隊列未滿,生產消息,隨后通知lock上的等待線程 //每一次的消息生產,都會通知消費者 System.out.println(Thread.currentThread().getName() + " : add a message"); messageQueue.addLast(message); //生產后,增加了消息,非空條件滿足,需要喚醒消費者 notEmptyCondition.signalAll(); } finally { } lock.unlock(); } public void get() { lock.lock(); try { //如果隊列為空,進入等待,無法獲取消息 while (messageQueue.isEmpty()) { try { System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting..."); //如果空了,消費者需要在“非空”條件上等待 notEmptyCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } //隊列非空時,讀取消息,隨后通知lock上的等待線程 //每一次的消息讀取,都會通知生產者 System.out.println(Thread.currentThread().getName() + " : get a message"); messageQueue.removeFirst(); //消費后,減少了消息,所以非滿條件滿足,需要喚醒生產者 notFullCondition.signalAll(); } finally { lock.unlock(); } } } }
總結
通過上面的示例,應該可以理解顯式鎖的思路
他與隱式鎖並沒有像名稱上看起來這么對立(一個隱 一個顯),他們的核心仍舊是為了解決線程的同步與線程間的通信協作
線程同步與通信的在Java中的底層核心概念為鎖和監視器
不管是synchronized還是Lock,不管是Object提供的通信方法還是Condition中的方法,都還是圍繞着鎖和監視器的概念展開的
如同平時寫代碼,同樣的功能,可能會有多種實現方式,顯式鎖和隱式鎖也是類似的,他們的實現有着很多的不同,也都有各種利弊
所以才會有隱式鎖和顯式鎖,在程序中很難找到“放之四海而皆准”的實現代碼,所以才會有各種各樣的解決方案
盡管早期synchronized關鍵字性能比較低,但是隨着版本的升級,性能也有了很大的改善
所以官方也是建議如果場景滿足,還是盡可能使用synchronized關鍵字而不是顯式鎖
顯式鎖是為了解決隱式鎖而不好解決的一些場景而存在的,盡管本文並沒有體現出來他們之間的差異(本文恰恰相反,對相同點進行了介紹)
但是顯式鎖有很多隱式鎖不存在的優點,后續慢慢介紹,通過本文希望理解,顯式鎖也只是線程同步與協作通信的一種實現途徑而已