java多線程之Read-Write Lock模式

一、Read-Write Lock模式

學生們正在看老師黑板上的板書,這時老師想擦掉板書,寫新的內容,這時學生們說:老師,我們還沒有看完,請先不要擦掉,於是老師就會等待大家都看完。

當線程讀取實例的狀態時,實例的狀態不會發生變化,實例的狀態僅在線程執行寫入的操作時纔會發生變化。從實例的狀態變化這個觀點來看,讀取和寫入有着本質的區別。

在Read-Write Lock模式中,讀取和寫入操作時分開的,在執行讀取操作之前,線程必須獲取於讀寫的鎖,而在執行寫入操作之前,線程必須獲取用於寫入的鎖。

由於線程執行讀取操作時,實例的狀態不會發生變化,所以多個線程可以同時讀取。但讀取時,不可以寫入。當線程執行寫入操作時,實例的狀態就會發生變化,因此,當有一個線程正在寫入時,其他線程不可以讀取或寫入。

一般來說,執行互斥處理會降低程序性能,但如果把針對寫入的互斥處理和針對讀取的互斥處理分開來考慮,則可以提高程序性能

二、示例程序

這裏編寫是多個線程對Data類的實例執行讀寫操作的程序。

類名 說明
Main.java 測試程序的類
Data.java 操作讀寫的類
WriterThread.java 表示寫入線程程序的類
ReaderThread.java 表示讀取線程程序的類
ReadWriteLock.java 表示操控讀寫鎖的類

實例程序的類圖:

img

1.Main類

這裏啓動6個讀取線程和2個寫入線程。

package com.viagra.Read_Write_Lock_Pattern.Lesson1;

/**
 * @Auther: viagra
 * @Date: 2019/11/19 17:32
 * @Description:
 */
public class Main {
    public static void main(String[] args) {
        /**
         * 這裏啓動6個讀取線程和2個寫入線程
         */
        Data data = new Data(10);
        new ReaderThread(data).start();
        new ReaderThread(data).start();
        new ReaderThread(data).start();
        new ReaderThread(data).start();
        new ReaderThread(data).start();
        new ReaderThread(data).start();
        new WriterThread(data, "ABCDEFGHIJKLMNOPQRSTUVWXYZ").start();
        new WriterThread(data, "abcdefghijklmnopqrstuvwxyz").start();
    }
}

2.Data類

是可以執行讀取read和寫入write操作的類,buffer對象實際讀取的對象是char的數組,lock字段保存的是該模式的主角ReadWriteLock的實例。

package com.viagra.Read_Write_Lock_Pattern.Lesson1;

/**
 * @Auther: viagra
 * @Date: 2019/11/19 17:32
 * @Description:
 */
public class Data {
    /**
     * 是可以執行讀取read和寫入write操作的類
     * buffer對象實際讀取的對象是char的數組
     * lock字段保存的是該模式的主角ReadWriteLock的實例
     */
    private final char[] buffer;
    private ReadWriteLock lock = new ReadWriteLock();

    /**
     * 構造函數會根據參數傳入的長度來分配一個char數組,並初始化buffer字段,同時以*填滿buffer,爲初始值.
     *
     * @param size
     */
    public Data(int size) {
        this.buffer = new char[size];
        for (int i = 0; i < buffer.length; i++) {
            buffer[i] = '*';
        }
    }

    /**
     * read方法執行讀取操作,實際的讀取是通過doRead方法方法執行的,而doRead方法夾在lock.readLock和lock.readUnLock之間
     * read.readLock表示獲取用戶讀取的鎖
     * 而read.readUnLock表示釋放用戶讀取的鎖,finally操作表示不管遇到啥情況read.readLock都會被調用.
     *
     * @return
     * @throws InterruptedException
     */
    public char[] read() throws InterruptedException {
        lock.readLock();
        try {
            return doRead();
        } finally {
            lock.readUnLock();
        }
    }

    public void write(char c) throws InterruptedException {
        lock.writeLock();
        try {
            doWrite(c);
        } finally {
            lock.writeUnLock();
        }
    }

    /**
     * doRead方法用戶執行實際的讀取操作,該方法會創建一個新的char數組newbuf來複制buffer字段的內容,並返回newbuf
     *
     * @return
     */
    private char[] doRead() {
        char[] newBuf = new char[buffer.length];
        for (int i = 0; i < buffer.length; i++) {
            newBuf[i] = buffer[i];
        }
        slowly();
        return newBuf;
    }

    /**
     * doWrite方法用於執行實際的寫入操作,該方法會以參數傳入字符c來填滿buffer字段
     *
     * @param c
     */
    private void doWrite(char c) {
        for (int i = 0; i < buffer.length; i++) {
            buffer[i] = c;
            slowly();
        }
    }

    /**
     * 用戶輔助模擬消耗的操作
     */
    private void slowly() {
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3.WriterThread類

表示是對Data實例寫入的操作的線程,filler是一個字符串,程序會逐個讀取filler中的字符,並write到Data的實例中,每寫一次,線程就會在0~3000毫秒內隨即sleep一段時間,此外nextChar方法用戶獲取下一次應該寫如的字符。

package com.viagra.Read_Write_Lock_Pattern.Lesson1;

import java.util.Random;

/**
 * @Auther: viagra
 * @Date: 2019/11/19 17:32
 * @Description:
 */
public class WriterThread extends Thread {
    /**
     * 表示是對Data實例寫入的操作的線程,filler是一個字符串,程序會逐個讀取filler中的字符,並write到Data的實例中.
     * 每寫一次,線程就會在0~3000毫秒內隨即sleep一段時間,此外nextChar方法用戶獲取下一次應該寫如的字符.
     */
    private final Data data;
    private static final Random random = new Random();
    private final String filler;
    private int index = 0;

    public WriterThread(Data data, String filler) {
        this.data = data;
        this.filler = filler;
    }

    public void run() {
        try {
            while (true) {
                char c = nextChar();
                //System.out.println(Thread.currentThread().getName() + " wwwwwwwwwrite : " + c);
                data.write(c);
                Thread.sleep(random.nextInt(3000));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private char nextChar() {
        char c = filler.charAt(index);
        index++;
        if (index >= filler.length()) {
            index = 0;
        }
        return c;
    }
}

4.ReaderThread類

表示的執行讀取操作的線程,該類會循環調用data.read方法,並顯示讀取到的char數組。

package com.viagra.Read_Write_Lock_Pattern.Lesson1;

/**
 * @Auther: viagra
 * @Date: 2019/11/19 17:32
 * @Description:
 */
public class ReaderThread extends Thread {
    /**
     * 表示的執行讀取操作的線程,該類會循環調用data.read方法,並顯示讀取到的char數組.
     */
    private final Data data;

    public ReaderThread(Data data) {
        this.data = data;
    }

    public void run() {
        try {
            while (true) {
                char[] buffer = data.read();
                System.out.println(Thread.currentThread().getName() + " reads " + String.valueOf(buffer));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

5.ReadWriteLock類

package com.viagra.Read_Write_Lock_Pattern.Lesson1;

/**
 * @Auther: viagra
 * @Date: 2019/11/19 17:32
 * @Description:
 */
public class ReadWriteLock {
    /**
     * 有衝突
     * 當線程想要獲取讀取鎖時:
     * 1.如果有線程正在執行寫入,則等待,
     * 2.如果有線程正在執行讀取,則無需等待
     * 當線程想要獲取寫入鎖時:
     * 3.如果有線程正在執行寫入,則等待
     * 4.如果有線程正在執行讀取,則等待
     */
    private int readingReaders = 0; //實際正在讀取的線程個數
    private int waitingWriters = 0; //正在等待寫入線程的個數
    private int writingWriters = 0; //實際正在寫入線程的個數
    private boolean preferWriter = true; //若寫入優先,則爲true

    /**
     * 讀取
     * 如果寫入線程大於0或者寫入有true且等待的線程大於0,則等待
     *
     * @throws InterruptedException
     */
    public synchronized void readLock() throws InterruptedException {
        while (writingWriters > 0 || (preferWriter && waitingWriters > 0)) {
            wait();
        }
        readingReaders++; //實際正在讀取的線程個數加1
    }

    /**
     * 釋放讀取,讀取-1,notifyAll
     */
    public synchronized void readUnLock() {
        readingReaders--; //實際正在讀取的線程個數減1
        preferWriter = true;
        notifyAll();
    }

    /**
     * 寫入
     * 如果讀取線程大於0或者寫入線程大於0,則等待
     *
     * @throws InterruptedException
     */
    public synchronized void writeLock() throws InterruptedException {
        waitingWriters++; //正在等待寫入的線程數加1
        try {
            while (readingReaders > 0 || writingWriters > 0) {
                wait();
            }
        } finally {
            waitingWriters--; //正在等待寫入的線程數減1
        }
        writingWriters++; //實際正在寫入的線程加1
    }

    /**
     * 寫入釋放 -1 ,notifyAll
     */
    public synchronized void writeUnLock() {
        writingWriters--; //正在等待寫入的線程數減1
        preferWriter = false;
        notifyAll();
    }
}

6.運行結果

Thread-0 reads **********
Thread-5 reads **********
Thread-1 reads **********
Thread-2 reads **********
Thread-4 reads **********
Thread-3 reads **********
Thread-4 reads AAAAAAAAAA
Thread-3 reads AAAAAAAAAA
Thread-2 reads AAAAAAAAAA
Thread-1 reads AAAAAAAAAA
Thread-0 reads AAAAAAAAAA
Thread-5 reads AAAAAAAAAA
Thread-5 reads aaaaaaaaaa
Thread-4 reads aaaaaaaaaa
Thread-1 reads aaaaaaaaaa

readLock方法和writeLock方法都使用了Guarded Suspension模式,就是有守護模式。

readLock方法:

在線程開始讀取操作之前,readLock方法被調用,在線程執行讀取操作時,有其他線程正在做讀取操作沒有關係,但是不允許存在正在寫入操作的線程,其守護條件就是沒有線程正在做寫入操作。

writeLock方法:

在線程開始做寫入操作之前,writeLock方法就會被調用,其守護條件爲沒有線程正在執行讀取操作或寫入操作。

三、Read-Write Lock模式中的角色

1.Reader(讀者)

執行read操作。

2.Writer(寫者)

執行write操作。

3.SharedResouce(共享資源)

表示的是Reader和Writer角色二者共享的資源,在實例程序中,Data類爲SharedResouce。

4.ReadWriteLock(讀寫鎖)

ReadWriteLock角色提供了SharedResouce實現read操作和write操作時所需的鎖,在實例中,是爲ReadWriteLock類。

ReadWriteLock模式的類圖:

img
代碼案例

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章