写在前面:
Java中:
- 我们用这样的代码新建一个信号量:
Semaphore mutex = new Semaphore(1);
- P操作(wait)的代码为:
mutex.acquire();
- V操作(signal)的代码为:
mutex.release();
本文章的内容:
- 读者写者问题(读者优先)
- 读者写者问题(公平)
- 读者写者问题(写者优先)
及其:
- 思路
- 验证
读者优先
思路:
读者写者问题大概是:对于临界文件的访问,不允许写-写,不允许写-读,但允许读-读
核心难点在于:读者和读者之间是不互斥的——那么,单单使用一个wr读写互斥锁,是不足以满足需求的
我们的解决方案是:既然读者之间不互斥,我们可以让只有第一个进入的读者需要P(rw),只有最后一个退出的读者需要V(rw)
即:第二、第三…之后的读者都跳过了P(rw),从而不被阻塞!
考虑下面一种情况:
Writer一个不小心没抢过Reader,rw锁被Reader拿走。之后,如果Reader源源不断的到来,源源不断的跳过P(rw),源源不断的使得readerCnt++…
那么,直到最后一个Reader退出释放rw,所有的Writer将永远因为得不到rw而阻塞,即发生饥饿——体会一下,这是不是读者优先?
SharedData.java
public class SharedData {
public Semaphore rw = new Semaphore(1); // 读写锁(本质是个互斥锁)
public Semaphore mutex = new Semaphore(1); // 读者计数器の互斥锁
public int readerCnt = 0; // 读者计数器
}
Writer.java
public class Writer implements Runnable {
SharedData sharedData;
public Writer(SharedData sharedData){
this.sharedData = sharedData;
}
private void write(){
System.out.println(Thread.currentThread().getName() + " Writing...");
}
@Override
public void run() {
while (true){
try {
// 对于写者,直接用「读写锁」锁一下就行了,读者稍稍会麻烦一些 >_<
sharedData.rw.acquire();
write();
sharedData.rw.release();
// // 睡一小会儿,不然while死循环刷得太快了
Thread.sleep(new Random().nextInt(2));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Reader.java
public class Reader implements Runnable {
SharedData sharedData;
public Reader(SharedData sharedData){
this.sharedData = sharedData;
}
private void read(){
System.out.println(Thread.currentThread().getName() + " Reading...");
}
@Override
public void run() {
while (true){
try {
// 第一个(cnt==0)读者进行P操作
sharedData.mutex.acquire();
if(sharedData.readerCnt == 0){
sharedData.rw.acquire();
}
sharedData.readerCnt++;
sharedData.mutex.release();
// 开始读 >_<
read();
// 最后一个(cnt==0)读者进行V操作
sharedData.mutex.acquire();
sharedData.readerCnt--;
if(sharedData.readerCnt == 0){
sharedData.rw.release();
}
sharedData.mutex.release();
// 睡一小会儿,不然while死循环刷得太快了
Thread.sleep(new Random().nextInt(2));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Test.java
public class Test {
public static void main(String[] args) {
// 共享数据(信号量计数器,啥的...)
SharedData sharedData = new SharedData();
// 1写者
new Thread(new Writer(sharedData)).start();
// 9读者
new Thread(new Reader(sharedData)).start();
new Thread(new Reader(sharedData)).start();
new Thread(new Reader(sharedData)).start();
new Thread(new Reader(sharedData)).start();
new Thread(new Reader(sharedData)).start();
new Thread(new Reader(sharedData)).start();
new Thread(new Reader(sharedData)).start();
new Thread(new Reader(sharedData)).start();
new Thread(new Reader(sharedData)).start();
}
}
验证:
上面分析过,Reader源源不断地到来会导致Writer的饥饿
那么,我们安排1:9的写/读者比例,模拟这种场景——你会发现,Writer很难得到一次运行(下图)
再取极端的情况,删掉Reader的Thread.sleep
,让读者真正“源源不断”——Writer将完全饥饿(自行试一下吧!)
公平读写
思路:
上面的「读者优先」引入了一种思想:通过计数来实现跳过P(rw)——这非常巧妙
这里,我们再引入一种极为巧妙的思想:设置一个queue信号信号量,来实现一个等待队列
如何实现所谓“等待队列”的呢?不妨用一个例子,通俗地顺着走一下:(读 - 写 - 读)
- 未使用queue信号量:Reader到来拿走rw;Writer到来被阻塞;第二个Reader到来,因为readerCnt != 0而跳过P(rw)开始执行——所谓“读者优先”
- 使用queue信号量:Reader到来拿走queue,接着拿走rw后归还queue;Writer到来拿走queue,接着被P(rw)阻塞;第二个Reader到来,上来就被P(queue)阻塞,它何时才能执行呢?必须等到第一个Reader归还rw,Writer得到rw执行完毕后,归还queue,第二个Reader才得以执行——queue使第二个Reader强制等待,形成一个“Reader-Writer-第二个Reader”的固定执行顺序——所谓“等待队列”
SharedData.java
public class SharedData {
public Semaphore rw = new Semaphore(1); // 读写锁(本质是个互斥锁)
public Semaphore mutex = new Semaphore(1); // 读者计数器の互斥锁
public Semaphore queue = new Semaphore(1); // 队列锁
public int readerCnt = 0; // 读者计数器
}
Writer.java
public class Writer implements Runnable {
SharedData sharedData;
public Writer(SharedData sharedData){
this.sharedData = sharedData;
}
private void write(){
System.out.println(Thread.currentThread().getName() + " Writing...");
}
@Override
public void run() {
while (true){
try {
// 核心逻辑
sharedData.queue.acquire();
sharedData.rw.acquire();
write();
sharedData.rw.release();
sharedData.queue.release();
// 睡一觉zzz
Thread.sleep(new Random().nextInt(2));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Reader.java
public class Reader implements Runnable {
SharedData sharedData;
public Reader(SharedData sharedData){
this.sharedData = sharedData;
}
private void read(){
System.out.println(Thread.currentThread().getName() + " Reading...");
}
@Override
public void run() {
while (true){
try {
// 第一个(cnt==0)读者进行P操作
sharedData.queue.acquire();
sharedData.mutex.acquire();
if(sharedData.readerCnt == 0){
sharedData.rw.acquire();
}
sharedData.readerCnt++;
sharedData.mutex.release();
sharedData.queue.release();
// 开始读 >_<
read();
// 最后一个(cnt==0)读者进行V操作
sharedData.mutex.acquire();
sharedData.readerCnt--;
if(sharedData.readerCnt == 0){
sharedData.rw.release();
}
sharedData.mutex.release();
// 睡一觉zzz
Thread.sleep(new Random().nextInt(2));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Test.java
public class Test {
public static void main(String[] args) {
// 共享数据(信号量,计数器啥的...)
SharedData sharedData = new SharedData();
// 5写者
new Thread(new Writer(sharedData)).start();
new Thread(new Writer(sharedData)).start();
new Thread(new Writer(sharedData)).start();
new Thread(new Writer(sharedData)).start();
// 5读者
new Thread(new Reader(sharedData)).start();
new Thread(new Reader(sharedData)).start();
new Thread(new Reader(sharedData)).start();
new Thread(new Reader(sharedData)).start();
}
}
验证:
既然已经实现了读写公平,那么我们便安排5:5的写/读者比例即可
很直观地可以看出,读者写者得到了“公平”的待遇(下图)
写者优先
思路:
这是读者优先所用思想的再一次运用。为什么Reader得以优先呢?
- 因为只有第一个Reader会被rw阻塞,之后来到的Reader可以跳过P(rw);而所有的Writer都会被rw阻塞——这就是Reader得到的“特权”
仿写句子:如何让Writer得以优先呢?
- 我们让只有第一个Writer会被queue阻塞,之后来到的Writer可以跳过P(queue);而所有的Reader都会被queue阻塞——这就是Writer得到的“特权”
SharedData.java
public class SharedData {
public Semaphore rw = new Semaphore(1); // 读写锁(本质是个互斥锁)
public Semaphore mutex = new Semaphore(1); // 读者计数器の互斥锁
public Semaphore mutex2 = new Semaphore(1); // 写者计数器の互斥锁
public Semaphore queue = new Semaphore(1); // 队列锁
public int readerCnt = 0; // 读者计数器
public int writerCnt = 0; // 写者计数器
}
Writer.java
public class Writer implements Runnable {
SharedData sharedData;
public Writer(SharedData sharedData){
this.sharedData = sharedData;
}
private void write(){
System.out.println(Thread.currentThread().getName() + " Writing...");
}
@Override
public void run() {
while (true){
try {
// 只有第一个进入的写者才需要排队
sharedData.mutex2.acquire();
if(sharedData.writerCnt == 0){
sharedData.queue.acquire();
}
sharedData.writerCnt++;
sharedData.mutex2.release();
sharedData.rw.acquire();
write();
sharedData.rw.release();
// 最后一个退出的写者释放queue
sharedData.mutex2.acquire();
sharedData.writerCnt--;
if(sharedData.writerCnt == 0){
sharedData.queue.release();
}
sharedData.mutex2.release();
// 睡一觉zzz
Thread.sleep(new Random().nextInt(2));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Reader.java
public class Reader implements Runnable {
SharedData sharedData;
public Reader(SharedData sharedData){
this.sharedData = sharedData;
}
private void read(){
System.out.println(Thread.currentThread().getName() + " Reading...");
}
@Override
public void run() {
while (true){
try {
// 所有的读者会被queue阻塞
sharedData.queue.acquire();
sharedData.mutex.acquire();
if(sharedData.readerCnt == 0){
sharedData.rw.acquire();
}
sharedData.readerCnt++;
sharedData.mutex.release();
sharedData.queue.release();
// 开始读 >_<
read();
sharedData.mutex.acquire();
sharedData.readerCnt--;
if(sharedData.readerCnt == 0){
sharedData.rw.release();
}
sharedData.mutex.release();
// 睡一觉zzz
Thread.sleep(new Random().nextInt(2));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Test.java
public class Test {
public static void main(String[] args) {
// 共享数据(信号量,计数器啥的...)
SharedData sharedData = new SharedData();
// 1读者
new Thread(new Reader(sharedData)).start();
// 9写者
new Thread(new Writer(sharedData)).start();
new Thread(new Writer(sharedData)).start();
new Thread(new Writer(sharedData)).start();
new Thread(new Writer(sharedData)).start();
new Thread(new Writer(sharedData)).start();
new Thread(new Writer(sharedData)).start();
new Thread(new Writer(sharedData)).start();
new Thread(new Writer(sharedData)).start();
new Thread(new Writer(sharedData)).start();
}
}
验证:
和「读者优先」一样的思路。我们设置1:9的读/写比例,模拟“源源不断”的Writer——Reader难以得到一次运行(下图)
再取极端的情况,删掉Writer的Thread.sleep
,让读者真正“源源不断”——Reader将完全饥饿(自行试一下吧!)
上面为了能直观展示Writer/Reader的执行情况,仅仅打印了一个“writing/reading”作为演示
如果想成为一个真正的读者/写者,可以将write()
/read()
方法中的代码替换成:
write()
String path = "/Users/你的自定义文件路径/共享文件.txt";
try {
// 从外部获取一行并写入文件(自动换行)
BufferedWriter bw = new BufferedWriter(new FileWriter(new File(path), true));
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
bw.write(br.readLine());
bw.write("\n");
br.close();
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
read()
String path = "/Users/你的自定义文件路径/共享文件.txt";
try {
// 从文件读取一行并打印出来(如果文本为空则打印null)
BufferedReader br = new BufferedReader(new FileReader(new File(path)));
String line = br.readLine();
System.out.println(line);
br.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
}
E
N
D
END
END