前言

Read-Write Lock,即读写锁模式。在学到这个模式的时候,不禁让我想起高中时的一个场景,在下课时,当我们一起抄黑板上的笔记,如果有一个人要上去将黑板擦了或修改时,我们就会制止他。而等我们一起抄完后,便允许他去修改或擦掉。这个场景和我们接下来要介绍的设计模式非常相像,即我们大家一起读可以,这时不允许有人去对内容做修改。而如果有人做修改时,则不允许别的人去做修改或读取。所以我们现在要整理一下会出现的情况,这样就非常方便我们接下来去学习这个模式。

冲突情况

第一种、即一个人(线程)读取时,另外一个人(线程)也可以读取。

第二种、即一个人(线程)读取时,另外一个人(线程)不允许写入。

第三种、即一个人(线程)写入时,另外一个人(线程)不允许读取。

第四中、即一个人(线程)写入时,另外一个人(线程)不允许写入。

读取写入
读取无冲突"读取"和"写入冲突"
写入"读取"和"写入冲突""写入"和"写入冲突"

学习场景

既然这样,我们就接着上面的场景,来学习这个模式吧。首先我们需要三个角色,一是被修改的数据,一个是写入线程,一个是读取线程。分别对应上面的黑板,老师,同学。

类图关系

image-20201021004437046
在想完上面的场景后,我们开始设计和学习吧。

角色设计

首先我们先设计黑板这个角色,因为他是被修改的数据,我们得先有数据。而黑板上的初始内容是一个字符数组,内容为 " * "。

黑板
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();


    }

image-20201021012508166
可以发现结果正确,并没有出现错误结果的情况。

结尾语

哈哈,就这样我们学会这个看似高大上的设计模式,8过如此,8过如此。如果喜欢的我的文章,记得给我点赞哟。


Fudada
0 声望1 粉丝

喜欢画画,喜欢后端。