锁Lock 那点事儿
項目經理今天又接了一個客戶需求,又要折磨我們這些程序員屌絲了。這個需求說起來很簡單,做起來非常容易出錯。我先簡單描述一下:
這是一個在線文件編輯器。同一份文件,一個人在讀的時候,其他人不能寫;同理,一個人在寫的時候,其他人也不能讀。也就是說,要么讀,要么寫,這兩件事情不能同時進行。
項目經理跟客戶講,“這個很容易實現的,我們是可以做的。”。什么都可以做,做不出來說是我們程序員能力不行,他一點責任都沒有。領導發話了,不管怎么樣,事情還是要做的。
看了一下需求,有兩個問題,我得先問清楚,否則到時候做得不對,他又把負責推給我,我們項目經理經常搞這些讓我背黑鍋的事情。
“多人同時讀可以嗎?”
“當然可以啦!多少人來讀都沒關系,文件的內容不要變就行。”。
“多人同時寫可以嗎?”
“當然不行啦!你寫別人也會寫,文件不知道以哪份數據為準了。”。
他態度極其惡劣,算了,不跟他計較了,我的項目獎金還在他手里。趕緊完工,下班了還要回家抱小孩。
根據多年的項目實戰經驗,我寫了一個超牛逼的 Data 類,來封裝文件的數據。看起來是這樣的:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public class Data { ????private final char[] buffer; ????public Data(int size) { ????????this.buffer = new char[size]; ????????for (int i = 0; i < size; i++) { ????????????buffer[i] = '*'; ????????} ????} ????public String read() { ????????StringBuilder result = new StringBuilder(); ????????for (char c : buffer) { ????????????result.append(c); ????????} ????????sleep(100); ????????return result.toString(); ????} ????public void write(char c) { ????????for (int i = 0; i < buffer.length; i++) { ????????????buffer[i] = c; ????????????sleep(100); ????????} ????} ????private void sleep(long ms) { ????????try { ????????????Thread.sleep(ms); ????????} catch (InterruptedException e) { ????????????e.printStackTrace(); ????????} ????} } |
稍微解釋一下:
當然了,以上這個示例跑通了,我想項目經理那個需求也不難實現。這也是我們平時做開發的一種習慣,先快速地寫個 Demo 出來,讓領導們看看,技術上走通了,我們再實現具體的需求。
好了,不就是要同時讀寫嗎?這不就是一個典型的多線程使用場景嗎?于是我快速地寫了一個讀取線程,讓它拼命地去讀取 Data 中的數據。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class ReaderThread extends Thread { ????private final Data data; ????public ReaderThread(Data data) { ????????this.data = data; ????} ????@Override ????public void run() { ????????while (true) { ????????????String result = data.read(); ????????????System.out.println(Thread.currentThread().getName() + " => " + result); ????????} ????} } |
在 ReaderThread 中通過一個死循環去不斷地讀取 Data 中的數據,并將結果打印出來。
再來一個寫入線程,讓它使勁地向 Data 中寫入數據。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public class WriterThread extends Thread { ????private final Data data; ????private final String str; ????private int index = 0; ????public WriterThread(Data data, String str) { ????????this.data = data; ????????this.str = str; ????} ????@Override ????public void run() { ????????while (true) { ????????????char c = next(); ????????????data.write(c); ????????} ????} ????private char next() { ????????char c = str.charAt(index); ????????index++; ????????if (index >= str.length()) { ????????????index = 0; ????????} ????????return c; ????} } |
一次性可以傳入一個字符串到?WriterThread 中,它將不斷獲取下一個字符(請見 next() 方法),并將該字符寫入 Data 中。
如果讓?ReaderThread 與?WriterThread?同時工作會怎樣?不妨寫了一個簡單的 Client 類運行試試看。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class Client { ????public static void main(String[] args) { ????????Data data = new Data(10); ????????new ReaderThread(data).start(); ????????new ReaderThread(data).start(); ????????new ReaderThread(data).start(); ????????new ReaderThread(data).start(); ????????new ReaderThread(data).start(); ????????new WriterThread(data, "ABCDEFGHI").start(); ????????new WriterThread(data, "012345789").start(); ????} } |
我開啟了 5 個?ReaderThread 與 2 個?WriterThread,模擬讀得多寫得少的情況,并將不同的數據寫入 Data 中。
運行一下!
…
Thread-1 => AA0A0A00A0
Thread-4 => AA0A0A00A0
Thread-3 => AA0A0A00A0
Thread-2 => AA0A0A00A0
Thread-0 => AA0A0A00A0
…
為何每次讀取出來的數據不一致呢?應該是輸出 10 個相同的字符才對啊!Data 的 buffer 中每個字符不是應該相同嗎?
如果把這個結果給項目經理看,他肯定要搞死我的。
哦!想到了!在多線程開發中,資源的訪問一定要做到“共享互斥”,也就是說要“上鎖”,這招還是架構師前幾天才教我的,我怎能不用?
于是我用了 Java 多線程中超牛逼的?synchronized 關鍵字,將它放到了 read() 與 write() 方法上,這樣就可以保證?synchronized 方法在同一時刻只能被一個線程調用了,其他線程將會阻擋在外。
廢話少說,趕緊加兩個 synchronized 運行看看吧。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class Data { ????... ????public synchronized String read() { ????????... ????} ????public synchronized void write(char c) { ????????... ????} ????... } |
再運行一把!
…
Thread-0 => 1111111111
Thread-4 => CCCCCCCCCC
Thread-3 => CCCCCCCCCC
Thread-2 => CCCCCCCCCC
Thread-1 => CCCCCCCCCC
…
終于搞定啦!這下子項目經理應該滿意了吧?
“不錯!這效果很好啊,同時寫同時讀,而且每次讀出來的數據都一樣,技術上應該是走通了,這個需求應該可以實現了吧?” 項目經理問。
“沒問題啊!小意思!” 我高興的答。
“這是一個在線文件編輯器,你考慮過性能問題嗎?” 架構師突然問了一句。
“性能很好啊!”
“你可以在 ReaderThread 中每調用?10 次 read() 方法,就打印 1 次所耗時間看看。”
“好啊!”
這還不簡單,我快速地給 ReaderThread 的 run() 方法中加了幾行代碼,測試一下運行所消耗的時間。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class ReaderThread extends Thread { ????... ????@Override ????public void run() { ????????while (true) { ????????????long begin = System.currentTimeMillis(); ????????????for (int i = 0; i < 10; i++) { ????????????????String result = data.read(); ????????????????System.out.println(Thread.currentThread().getName() + " => " + result); ????????????} ????????????long time = System.currentTimeMillis() - begin; ????????????System.out.println(Thread.currentThread().getName() + " -- " + time + "ms"); ????????} ????} } |
跑起來吧!
…
Thread-2 => IIIIIIIIII
Thread-2 — 24802ms
Thread-3 => IIIIIIIIII
Thread-3 — 24901ms
Thread-4 => IIIIIIIIII
Thread-4 — 25001ms
Thread-0 => 3333333333
…
Thread-0 => 1111111111
Thread-0 — 55305ms
Thread-4 => CCCCCCCCCC
Thread-3 => CCCCCCCCCC
Thread-2 => CCCCCCCCCC
Thread-1 => CCCCCCCCCC
Thread-1 — 58705ms
Thread-2 => CCCCCCCCCC
…
我隨意挑選了其中這 5 個?ReaderThread 所消耗的時間,平均值是:37742.8 毫秒,折合 37.8 秒。
我心里也沒譜了,這性能到底是否需要優化呢?于是我帶著測試結果,去向架構師請教。
他看到了這樣的結果,微笑著搖了搖頭。從他鄙視而又猥瑣的表情上,我可以推測,這次他又要在我面前露一手了。
來吧,我給你寫一個 ReadWriteLock,你自己去看吧。
隨后,架構師用他熟練的手指,瘋狂地在鍵盤上敲了一堆讓我一知半解的東西。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | ublic class ReadWriteLock { ????private int readThreadCounter = 0;????? // 正在讀取的線程數(0個或多個) ????private int waitingWriteCounter = 0;??? // 等待寫入的線程數(0個或多個) ????private int writeThreadCounter = 0;???? // 正在寫入的線程數(0個或1個) ????private boolean writeFlag = true;?????? // 是否對寫入優先(默認為是) ????// 讀取加鎖 ????public synchronized void readLock() throws InterruptedException { ????????// 若存在正在寫入的線程,或當寫入優先時存在等待寫入的線程,則將當前線程設置為等待狀態 ????????while (writeThreadCounter > 0 || (writeFlag && waitingWriteCounter > 0)) { ????????????wait(); ????????} ????????// 使正在讀取的線程數加一 ????????readThreadCounter++; ????} ????// 讀取解鎖 ????public synchronized void readUnlock() { ????????// 使正在讀取的線程數減一 ????????readThreadCounter--; ????????// 讀取結束,對寫入優先 ????????writeFlag = true; ????????// 通知所有處于 wait 狀態的線程 ????????notifyAll(); ????} ????// 寫入加鎖 ????public synchronized void writeLock() throws InterruptedException { ????????// 使等待寫入的線程數加一 ????????waitingWriteCounter++; ????????try { ????????????// 若存在正在讀取的線程,或存在正在寫入的線程,則將當前線程設置為等待狀態 ????????????while (readThreadCounter > 0 || writeThreadCounter > 0) { ????????????????wait(); ????????????} ????????} finally { ????????????// 使等待寫入的線程數減一 ????????????waitingWriteCounter--; ????????} ????????// 使正在寫入的線程數加一 ????????writeThreadCounter++; ????} ????// 寫入解鎖 ????public synchronized void writeUnlock() { ????????// 使正在寫入的線程數減一 ????????writeThreadCounter--; ????????// 寫入結束,對讀取優先 ????????writeFlag = false; ????????// 通知所有處于等待狀態的線程 ????????notifyAll(); ????} } |
我看出來了,架構師特意寫了很多注釋,免得我總是去煩他。
代碼不解釋了,看看注釋吧,有疑問可以給我留言哦!
此時,Data 類還需要稍作改寫。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | public class Data { ????... ????private final ReadWriteLock lock = new ReadWriteLock(); // 創建讀寫鎖 ????... ????public String read() throws InterruptedException { ????????lock.readLock(); // 讀取上鎖 ????????try { ????????????return doRead(); // 執行讀取操作 ????????} finally { ????????????lock.readUnlock(); // 讀取解鎖 ????????} ????} ????public void write(char c) throws InterruptedException { ????????lock.writeLock(); // 寫入上鎖 ????????try { ????????????doWrite(c); // 執行寫入操作 ????????} finally { ????????????lock.writeUnlock(); // 寫入解鎖 ????????} ????} ????private String doRead() { ????????StringBuilder result = new StringBuilder(); ????????for (char c : buffer) { ????????????result.append(c); ????????} ????????sleep(100); ????????return result.toString(); ????} ????private void doWrite(char c) { ????????for (int i = 0; i < buffer.length; i++) { ????????????buffer[i] = c; ????????????sleep(100); ????????} ????} ????... } |
同樣的 Client 類,我再運行一把試試看,性能是否有提高呢?
…
Thread-1 => 4444444444
Thread-2 — 14000ms
Thread-0 — 14001ms
Thread-3 — 14000ms
Thread-4 — 14000ms
Thread-1 — 14001ms
Thread-4 => IIIIIIIIII
…
平均下來是?14000.4 毫秒,折合 14.0 秒,比以前快了 63%,而且輸出的結果都比以前平穩(以前忽高忽低的)。
果然是架構師,真讓我們這些程序員崇拜啊!
最后架構師過來,看到我在那里得意地笑。他拍拍我的肩,對我說:“別樂了,其實 JDK 1.5 中已經有?ReadWriteLock 了,我這個只不過是一個精簡版而已,去看看 java.util.concurrent.locks.ReadWriteLock 吧,你一定會震精!”。
看來我真是孤陋寡聞啊,打開 JDK API 看到了 ReadWriteLock:
| 1 2 3 4 5 6 | public interface ReadWriteLock { ????Lock readLock(); ????Lock writeLock(); } |
可以通過 ReadWriteLock 接口來獲取 ReadLock 與 WriteLock,它們都是?Lock 對象,這也是一個接口。
官方提供了一個 ReadWriteLock 接口的實現類?java.util.concurrent.locks.ReentrantReadWriteLock。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public interface Lock { ????void lock(); ????void lockInterruptibly() throws InterruptedException; ????boolean tryLock(); ????boolean tryLock(long time, TimeUnit unit) throws InterruptedException; ????void unlock(); ????Condition newCondition(); } |
該接口中,有兩個非常重要的方法:lock() 與?unlock(),分別表示“上鎖”與“解鎖”。
嘗試用一下 JDK 的?ReadWriteLock 吧。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public class Data { ????... ????private final ReadWriteLock lock = new ReentrantReadWriteLock(); // 創建讀寫鎖 ????private final Lock readLock = lock.readLock();??? // 獲取讀鎖 ????private final Lock writeLock = lock.writeLock();? // 獲取寫鎖 ????... ????public String read() throws InterruptedException { ????????readLock.lock(); // 讀取上鎖 ????????try { ????????????return doRead(); // 執行讀取操作 ????????} finally { ????????????readLock.unlock(); // 讀取解鎖 ????????} ????} ????public void write(char c) throws InterruptedException { ????????writeLock.lock(); // 寫入上鎖 ????????try { ????????????doWrite(c); // 執行寫入操作 ????????} finally { ????????????writeLock.unlock(); // 寫入解鎖 ????????} ????} ????... } |
再次運行一把看看效果。
使用了 JDK 的 ReadWriteLock,性能與自己實現的?ReadWriteLock 差不多,大家不妨自己試一下吧。
此外 JDK 還提供了一個更加簡單的?ReentrantLock,它可以取代 synchronized,確保獲取更高的吞吐率,一般可以這樣來做:
以前的做法:
| 1 2 3 | public synchronized void foo() { ????... } |
現在的做法:
| 1 2 3 4 5 6 7 8 9 10 | private final Lock lock = new ReentrantLock(); public void foo() { ????lock.lock(); ????try { ????????... ????} finally { ????????lock.unlock(); ????} } |
這里提供兩張?synchronized 與 Lock 的性能測試對比:
參考:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html
總結
當系統中出現不同的讀寫線程同時訪問某一資源時,需要考慮共享互斥問題,可使用?synchronized 解決次問題。若對性能要求較高的情況下,可考慮使用 ReadWriteLock 接口及其 ReentrantReadWriteLock?實現類,當然,自己實現一個 ReadWriteLock 也是一種解決方案。此外,為了在高并發情況下獲取較高的吞吐率,建議使用 Lock 接口及其?ReentrantLock 實現類來替換以前的 synchronized 方法或代碼塊。
關于 Lock 那點事兒當然還不止這些,今天先寫到這里吧,以上內容是否對大家有用,敬請點評!
原文出處:?黃勇
from: http://www.importnew.com/22971.html
總結
以上是生活随笔為你收集整理的锁Lock 那点事儿的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在Spring MVC中使用Apache
- 下一篇: 使用基于注解的mybatis时,利用反射