所謂讀寫鎖,即是讀鎖和寫鎖的統稱,它是兩種鎖,但放在同一個對象里,通過兩個方法分別獲取。適用場景是讀多寫少的業務,比如緩存。用法很簡單,三原則:讀讀共享、讀寫互斥、寫寫互斥。換種說法:讀鎖是共享的,讀鎖允許其他線程的讀操作,而寫鎖是互斥的,寫鎖不允許其他線程的讀寫操作。
但此處有一個問題先提出來:正所謂一山不容二虎,讀與寫這二虎相爭,必有一王。當讀寫並存時,我們只能取一個,取哪一個呢?正如上面說到的,讀寫鎖使用場景是讀多寫少,如果一個寫線程進來,而讀線程很多,結果必然是寫線程將苦逼的一直等待中,它會因得不到資源而產生飢渴。我們要做的,應該是保證請求寫操作的線程不會被后來的讀線程擠掉。看實現:
package com.wulinfeng.test.testpilling.util; /** * 實現讀寫鎖 * * @author wulinfeng * @version C10 2019年1月10日 * @since SDP V300R003C10 */ public class MyReadWriteLock { // 讀線程 private int read = 0; // 寫線程 private int write = 0; // 寫請求線程 private int writeRequest = 0; /** * 加讀鎖 * * @author wulinfeng * @throws InterruptedException */ public synchronized void readLock() throws InterruptedException { // 有寫或者寫請求線程,則讓讀線程等一等 while (write > 0 || writeRequest > 0) { wait(); } // 獲取到讀鎖,累加 read++; } /** * 加寫鎖 * * @author wulinfeng * @throws InterruptedException */ public synchronized void writeLock() throws InterruptedException { // 寫請求累加 writeRequest++; // 若無寫線程獨占且無讀線程,則寫請求變成寫操作 while (write > 0 || read > 0) { wait(); } // 到這里表示獲取到了所,寫請求自減 writeRequest--; write++; } /** * 解讀鎖 * * @author wulinfeng */ public synchronized void readUnlock() { read--; notifyAll(); } /** * 解寫鎖 * * @author wulinfeng */ public synchronized void writeUnlock() { write--; notifyAll(); } }
這里分別用讀和寫的加鎖、解鎖方法簡化了,沒有使用讀鎖和寫鎖兩個對象。當前如果有讀線程,那么寫線程會先等它讀完,但不允許后面的讀線程繼續進來。若已存在寫操作或寫請求線程,后面來的讀線程就會被掛起。很明顯,這里通過區分出一個寫請求線程來解決寫線程的飢餓問題。看下測試:
package com.wulinfeng.test.testpilling; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.util.concurrent.CountDownLatch; import org.junit.Before; import org.junit.Test; import com.wulinfeng.test.testpilling.util.MyReadWriteLock; public class MyReadWriteLockTest { // 文件名 private static final String FILE_PATH = "D:\\hello.txt"; // 文件 private final File file = new File(FILE_PATH); // 讓Junit支持多線程,3個線程就先初始化3 private CountDownLatch latch = new CountDownLatch(3); @Before public void createFile() throws IOException { // 建文件 if (!file.exists()) { System.out.println("文件不存在。"); file.createNewFile(); // 寫初始內容 StringBuilder sb = new StringBuilder(); sb.append("細雨之中的西湖、盛開的荷花、一座斷橋,淡淡幾筆,足以勾勒出淡妝濃抹總相宜的杭州。\n") .append("杭州之美,在於留給旁人對美的無盡想象空間。對我們大多數人來說,杭州的美,猶如一幅盛滿故事的山水畫,詩意、神秘、動情;對於航天之父錢學森來說,杭州於他也是這般美麗。\n") .append("只是直到19歲,他才能借養病之機認識自己家鄉的美麗,到底有點遲了。\n") .append("但錢學森一觸摸到杭州這幅美卷,便充滿不舍和一生的惦念。\n") .append( "雖然錢學森年少時因父親工作調動而輾轉生活於北京、上海,長大后為了學業穿梭於北京和大洋彼岸的美國,但杭州,終究是錢學森生命開始的地方,這里聽到過他的第一聲啼哭,雕刻過他的第一個腳印——他是踏蓮而生的,他先着地的雙腳,讓杭州望族錢家越發枝葉繁茂。\n"); BufferedWriter bw = null; try { bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(FILE_PATH, true), "GBK")); bw.write(sb.toString()); } catch (IOException e) { e.printStackTrace(); } finally { try { bw.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Test public void TestMyReadWriteLock() { MyReadWriteLock lock = new MyReadWriteLock(); // 起個線程讀 // 讀文件 new Thread(() -> { { BufferedReader br = null; // 加鎖 try { lock.readLock(); } catch (InterruptedException e) { e.printStackTrace(); } // 讀文件 try { br = new BufferedReader(new InputStreamReader(new FileInputStream(FILE_PATH), "GBK")); int lineNo = 0; String lineContent = null; while ((lineContent = br.readLine()) != null) { System.out.printf("行號:%d:%s\n", lineNo, lineContent); lineNo++; } } catch (IOException e) { e.printStackTrace(); } finally { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } // 解鎖 lock.readUnlock(); } }).start(); latch.countDown(); // 起個線程寫,寫的內容可以多一點 new Thread(() -> { { String content = "人們對杭州的了解,更多地源自曾風靡一時的電視劇《新白娘子傳奇》,還有魯迅筆下那倒掉的雷峰塔。"; BufferedWriter bw = null; // 加鎖 try { lock.writeLock(); } catch (InterruptedException e) { e.printStackTrace(); } // 寫入 try { bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(FILE_PATH, true), "GBK")); bw.write(content); } catch (IOException e) { e.printStackTrace(); } finally { try { bw.close(); } catch (IOException e3) { e3.printStackTrace(); } } // 解鎖 lock.writeUnlock(); } }).start(); latch.countDown(); // 再起個線程讀,因為上面的寫線程存在,它將掛起 new Thread(() -> { { BufferedReader br = null; // 加鎖 try { lock.readLock(); } catch (InterruptedException e) { e.printStackTrace(); } // 讀文件 try { br = new BufferedReader(new InputStreamReader(new FileInputStream(FILE_PATH), "GBK")); int lineNo = 0; String lineContent = null; while ((lineContent = br.readLine()) != null) { System.out.printf("行號:%d:%s\n", lineNo, lineContent); lineNo++; } } catch (IOException e) { e.printStackTrace(); } finally { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } // 解鎖 lock.readUnlock(); } }).start(); latch.countDown(); // 主線程等待 try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } }
先看控制台輸出:
文件不存在。 行號:0:細雨之中的西湖、盛開的荷花、一座斷橋,淡淡幾筆,足以勾勒出淡妝濃抹總相宜的杭州。 行號:1:杭州之美,在於留給旁人對美的無盡想象空間。對我們大多數人來說,杭州的美,猶如一幅盛滿故事的山水畫,詩意、神秘、動情;對於航天之父錢學森來說,杭州於他也是這般美麗。 行號:2:只是直到19歲,他才能借養病之機認識自己家鄉的美麗,到底有點遲了。 行號:3:但錢學森一觸摸到杭州這幅美卷,便充滿不舍和一生的惦念。 行號:4:雖然錢學森年少時因父親工作調動而輾轉生活於北京、上海,長大后為了學業穿梭於北京和大洋彼岸的美國,但杭州,終究是錢學森生命開始的地方,這里聽到過他的第一聲啼哭,雕刻過他的第一個腳印——他是踏蓮而生的,他先着地的雙腳,讓杭州望族錢家越發枝葉繁茂。
再看生產的文件hello.txt,追加了我們寫入的那一行,但為啥后面的讀線程並未打印出來呢?
因為打印到控制台太耗時,還沒來得及打印完,主線程已經跑完了。所以我們需要在回到主線程前先休眠一會兒,讓讀操作執行完:
// 先休息一會兒 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 主線程等待 try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); }
// 先休息一會兒 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 主線程等待 try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); }
這次輸出可以看到后面的讀線程也執行了:
文件不存在。 行號:0:細雨之中的西湖、盛開的荷花、一座斷橋,淡淡幾筆,足以勾勒出淡妝濃抹總相宜的杭州。 行號:1:杭州之美,在於留給旁人對美的無盡想象空間。對我們大多數人來說,杭州的美,猶如一幅盛滿故事的山水畫,詩意、神秘、動情;對於航天之父錢學森來說,杭州於他也是這般美麗。 行號:2:只是直到19歲,他才能借養病之機認識自己家鄉的美麗,到底有點遲了。 行號:3:但錢學森一觸摸到杭州這幅美卷,便充滿不舍和一生的惦念。 行號:4:雖然錢學森年少時因父親工作調動而輾轉生活於北京、上海,長大后為了學業穿梭於北京和大洋彼岸的美國,但杭州,終究是錢學森生命開始的地方,這里聽到過他的第一聲啼哭,雕刻過他的第一個腳印——他是踏蓮而生的,他先着地的雙腳,讓杭州望族錢家越發枝葉繁茂。 行號:0:細雨之中的西湖、盛開的荷花、一座斷橋,淡淡幾筆,足以勾勒出淡妝濃抹總相宜的杭州。 行號:1:杭州之美,在於留給旁人對美的無盡想象空間。對我們大多數人來說,杭州的美,猶如一幅盛滿故事的山水畫,詩意、神秘、動情;對於航天之父錢學森來說,杭州於他也是這般美麗。 行號:2:只是直到19歲,他才能借養病之機認識自己家鄉的美麗,到底有點遲了。 行號:3:但錢學森一觸摸到杭州這幅美卷,便充滿不舍和一生的惦念。 行號:4:雖然錢學森年少時因父親工作調動而輾轉生活於北京、上海,長大后為了學業穿梭於北京和大洋彼岸的美國,但杭州,終究是錢學森生命開始的地方,這里聽到過他的第一聲啼哭,雕刻過他的第一個腳印——他是踏蓮而生的,他先着地的雙腳,讓杭州望族錢家越發枝葉繁茂。 行號:5:人們對杭州的了解,更多地源自曾風靡一時的電視劇《新白娘子傳奇》,還有魯迅筆下那倒掉的雷峰塔。