读写锁浅析
所谓读写锁,即是读锁和写锁的统称,它是两种锁,但放在同一个对象里,通过两个方法分别获取。适用场景是读多写少的业务,比如缓存。用法很简单,三原则:读读共享、读写互斥、写写互斥。换种说法:读锁是共享的,读锁允许其他线程的读操作,而写锁是互斥的,写锁不允许其他线程的读写操作。
但此处有一个问题先提出来:正所谓一山不容二虎,读与写这二虎相争,必有一王。当读写并存时,我们只能取一个,取哪一个呢?正如上面说到的,读写锁使用场景是读多写少,如果一个写线程进来,而读线程很多,结果必然是写线程将苦逼的一直等待中,它会因得不到资源而产生饥渴。我们要做的,应该是保证请求写操作的线程不会被后来的读线程挤掉。看实现:
package com.wulf.test.testpilling.util; /** * 实现读写锁 * * @author wulf * @since 2019年1月10日*/ public class MyReadWriteLock { // 读线程 private int read = 0; // 写线程 private int write = 0; // 写请求线程 private int writeRequest = 0; /** * 加读锁 * * @throws InterruptedException */ public synchronized void readLock() throws InterruptedException { // 有写或者写请求线程,则让读线程等一等 while (write > 0 || writeRequest > 0) { wait(); } // 获取到读锁,累加 read++; } /** * 加写锁 * * @throws InterruptedException */ public synchronized void writeLock() throws InterruptedException { // 写请求累加 writeRequest++; // 若无写线程独占且无读线程,则写请求变成写操作 while (write > 0 || read > 0) { wait(); } // 到这里表示获取到了所,写请求自减 writeRequest--; write++; } /** * 解读锁 **/ public synchronized void readUnlock() { read--; notifyAll(); } /** * 解写锁 **/ public synchronized void writeUnlock() { write--; notifyAll(); } }
这里分别用读和写的加锁、解锁方法简化了,没有使用读锁和写锁两个对象。当前如果有读线程,那么写线程会先等它读完,但不允许后面的读线程继续进来。若已存在写操作或写请求线程,后面来的读线程就会被挂起。很明显,这里通过区分出一个写请求线程来解决写线程的饥饿问题。看下测试:
package com.wulf.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.wulf.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:人们对杭州的了解,更多地源自曾风靡一时的电视剧《新白娘子传奇》,还有鲁迅笔下那倒掉的雷峰塔。