前言
Read-Write Lock,即读写锁模式。在学到这个模式的时候,不禁让我想起高中时的一个场景,在下课时,当我们一起抄黑板上的笔记,如果有一个人要上去将黑板擦了或修改时,我们就会制止他。而等我们一起抄完后,便允许他去修改或擦掉。这个场景和我们接下来要介绍的设计模式非常相像,即我们大家一起读可以,这时不允许有人去对内容做修改。而如果有人做修改时,则不允许别的人去做修改或读取。所以我们现在要整理一下会出现的情况,这样就非常方便我们接下来去学习这个模式。
冲突情况
第一种、即一个人(线程)读取时,另外一个人(线程)也可以读取。
第二种、即一个人(线程)读取时,另外一个人(线程)不允许写入。
第三种、即一个人(线程)写入时,另外一个人(线程)不允许读取。
第四中、即一个人(线程)写入时,另外一个人(线程)不允许写入。
读取 | 写入 | |
---|---|---|
读取 | 无冲突 | "读取"和"写入冲突" |
写入 | "读取"和"写入冲突" | "写入"和"写入冲突" |
学习场景
既然这样,我们就接着上面的场景,来学习这个模式吧。首先我们需要三个角色,一是被修改的数据,一个是写入线程,一个是读取线程。分别对应上面的黑板,老师,同学。
类图关系
在想完上面的场景后,我们开始设计和学习吧。
角色设计
首先我们先设计黑板这个角色,因为他是被修改的数据,我们得先有数据。而黑板上的初始内容是一个字符数组,内容为 " * "。
黑板
public class BlackBoard {
//黑板上的内容
private final char[] buffer;
// private final ReadWriteLock lock = new ReadWriteLock();
//读写锁
private final RWLock lock = new RWLock();
public BlackBoard(int size) {
//初始化数组
this.buffer = new char[size];
for (int i = 0; i < buffer.length; i++) {
//初始化内容
buffer[i] = '*';
}
}
public char[] read() throws InterruptedException {
//先获取读锁
lock.readLock();
try {
//执行读取操作,主要是将这个内容buffer打印出来
return doRead();
}finally {
//释放锁
lock.readUnLock();
}
}
/*
writeThread-2 --> a
正在写入的线程数为1
正在写入的线程数为1
writeThread-2:aaaaaaaaaa
readThread1 is reading : aaaaaaaaaa
*/
/*
这里值得我们注意的一个事情是:
这整个wirte()方法并不具有原子性,因为他会被打断的,比如说说上面的结果
我们已经上了writeLock()锁,传了字符 c过来,
但此时被读线程打断,输出了一句 “正在执行的线程数为 1 ”,但随即再执行下去就被阻塞住了,
再把控制权交还给写线程。导致读线程无法继续往下读。
然后写线程继续执行,并把buffer内容刷新后,再唤醒所有线程。保证了这个方法的线程安全性。
所以读出的结果是 aaaaa
但值得注意的是,锁的每一个方法都是具有原子性的。这点一定得理解好。
*/
public void wirte(char c) throws InterruptedException {
/*
下方的打点是为了测试
*/
lock.writeLock();
try{
/*
测试传过来的字符
*/
System.out.println(Thread.currentThread().getName() + " --> " + c);
doWrite(c);
/*
测试执行后的buffer的内容是啥
*/
System.out.println(Thread.currentThread().getName() + ":" + String.valueOf(buffer).toString());
}finally {
/*
释放写锁。
*/
lock.writeUnLock();
}
}
private void doWrite(char c) {
/*
写操作就是将写线程的字符串分解为字符数组
然后再将每一个字符全部填满buffer,每个字符试一遍。
*/
for (int i = 0; i < buffer.length; i++) {
buffer[i] = c;
slowly();
}
}
/*
* 读取操作是将旧char数组读到新的数组中去
* */
private char[] doRead() {
char[] newBuf = new char[buffer.length];
for (int i = 0; i < buffer.length; i++) {
newBuf[i] = buffer[i];
}
slowly();
return newBuf;
}
/*
暂停
*/
private void slowly() {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
设计完黑板后,我们可以看下读线程,也即是学生。
学生
public class Student extends Thread{
private final BlackBoard board;
public Student(BlackBoard blackBoard, String name) {
super(name);
this.board = blackBoard;
}
@Override
public void run() {
try {
while (true) {
char[] readBuf = board.read();
//循环打印黑板上的内容
System.out.println(Thread.currentThread().getName() + " is reading : " + String.valueOf(readBuf));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
老师
public class Teacher extends Thread{
private static final Random random = new Random();
private final BlackBoard data;
/*
传入的字符串
*/
private final String filler;
private int index = 0;
public Teacher(BlackBoard data, String filler, String name) {
super(name);
this.data = data;
this.filler = filler;
}
@Override
public void run() {
try {
while (true) {
char c = nextChar();
/*
将这个字符填满buffer
*/
data.wirte(c);
/*
随即睡眠
*/
Thread.sleep(random.nextInt(3000));
}
} catch (Exception e) {
e.printStackTrace();
}
}
//移动指针,获取字符串下一个字符
private char nextChar() {
char c = filler.charAt(index);
index++;
if (index >= filler.length()) {
index = 0;
}
return c;
}
}
读写锁
接下来我到我们最重要的设计,即是读取锁,我们定义了两个变量,readingNums代表为正在进行读取操作的读取线程数,也即是学生数。writingNums代表为写入线程数,也即是正在修改内容的老师。下面我们将在注释中说明各个设计的思路。
public class RWLock {
private int readingNums = 0;
private int writingNums = 0;
/*
这个方法是原子的
在我们要进行读取时,如果存在写线程,则会进行阻塞
被唤醒时,读取线程数++。
*/
public synchronized void readLock() {
System.out.println("正在写入的线程数为"+writingNums);
while (writingNums > 0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
readingNums++;
}
/*
释放读锁时,则唤醒所有被阻塞的线程
*/
public synchronized void readUnLock() {
readingNums--;
notifyAll();
}
/*
在我们进行写入时,如果存在写线程或者读线程时,则进行自我阻塞
在获得执行权时则进行自增
*/
public synchronized void writeLock() {
while (readingNums > 0 || writingNums > 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
}
}
writingNums++;
}
/*
释放写锁时,则写锁自减
唤醒所有线程
*/
public synchronized void writeUnLock() {
writingNums--;
notifyAll();
}
}
设计模式中原版的方法
public class ReadWriteLock {
//读取中的线程的个数
private int readingReaders = 0;
//正在等待写入线程的个数
private int waitingWriters = 0;
//写入中线程的个数
private int writingWriters = 0;
//true时写入优先,false读取优先
private boolean preferWriters = true;
public synchronized void readLock() throws InterruptedException {
//当存在写入线程时 或者 写入优先且存在阻塞的写入线程时,读线程就必须等待
while (writingWriters > 0 || (preferWriters && waitingWriters > 0)) {
wait();
}
//获得执行权时,自增
readingReaders++;
}
public synchronized void readUnLock() {
//读线程 -1
readingReaders-- ;
//读锁释放,写线程优先
preferWriters = true;
//唤醒所有线程
notifyAll();
}
public synchronized void writeLock() throws InterruptedException {
waitingWriters++;
try {
//当存在写锁或者读锁时
while (readingReaders > 0 || writingWriters > 0) {
wait();
}
} finally {
waitingWriters--;
}
writingWriters++;
}
public synchronized void writeUnLock() {
writingWriters--;
preferWriters = false;
notifyAll();
}
}
场景测试
错误结果:如果发生线程不安全的操作,buffer中的所有字符不一定完全相同。
正确结果:如果线程安全,buffer中的所有字符则一定全部相同。
public static void main(String[] args) {
BlackBoard data = new BlackBoard(10);
for (int i = 0; i < 2; i++) {
new Student(data,"readThread"+i).start();
}
new Teacher(data,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","writeThread-1").start();
new Teacher(data,"abcdefghijklmnopqrstuvwxyz","writeThread-2").start();
}
可以发现结果正确,并没有出现错误结果的情况。
结尾语
哈哈,就这样我们学会这个看似高大上的设计模式,8过如此,8过如此。如果喜欢的我的文章,记得给我点赞哟。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。