Multithread Design Patterns: Part 3 - Producer-Consumer Patterns and Read-Write Lock Patterns

Keywords: Java jvm

First, the producer-consumer model

The producer-consumer model is a common one. When both producers and consumers have only one model, it is also called the Pipe model, i.e. the pipeline model.

In the producer-consumer mode, data is transmitted to each other through Channel, which is the channel. Then, in what order the data is transmitted in the channel, we need to consider at the design time. The general implementation includes the following three ways:

  • Queue - Sequential Passing
  • Stack - Inverse Passing
  • Priority Queue - Passed by Weight/Priority

Channel channels can be implemented through BlockingQueue in the juc package, thus eliminating wait/notify operations when implementing Queue. An example of a simple producer-consumer model is given. In this example, the producer is responsible for producing the cake and putting it on the table. The consumer is responsible for taking the cake from the table. Here the table serves as a data transmission channel. The code is as follows:

/**
 * @author koma <komazhang@foxmail.com>
 * @date 2018-10-18
 */
public class Table {
    private final Queue<String> queue;
    private final int count;

    public Table(int count) {
        this.count = count;
        queue = new LinkedList<>();
    }

    public synchronized void put(String cake) throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+" puts "+cake);
        while (queue.size() >= count) {
            wait();
        }
        queue.offer(cake);
        notifyAll();
    }

    public synchronized String take() throws InterruptedException {
        while (queue.size() <= 0) {
            wait();
        }
        String cake = queue.poll();
        notifyAll();
        System.out.println(Thread.currentThread().getName()+" takes "+cake);
        return cake;
    }
}

The producer, consumer and startup class codes are as follows:

/**
 * @author koma <komazhang@foxmail.com>
 * @date 2018-10-18
 */
public class Main {
    public static void main(String[] args) {
        Table table = new Table(3);
        new MakerThread("MakerThread-1", table, 314151).start();
        new MakerThread("MakerThread-2", table, 523242).start();
        new MakerThread("MakerThread-3", table, 716151).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new EaterThread("EaterThread-1", table, 625341).start();
        new EaterThread("EaterThread-2", table, 525349).start();
        new EaterThread("EaterThread-3", table, 225841).start();
    }
}

public class EaterThread extends Thread {
    private final Random random;
    private final Table table;

    public EaterThread(String name, Table table, long seed) {
        super(name);
        this.table = table;
        this.random = new Random(seed);
    }

    @Override
    public void run() {
        try {
            while (true) {
                String cake = table.take();
                Thread.sleep(random.nextInt(1000));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class MakerThread extends Thread {
    private final Random random;
    private final Table table;
    private static int id = 0;

    public MakerThread(String name, Table table, long seed) {
        super(name);
        this.table = table;
        this.random = new Random(seed);
    }

    @Override
    public void run() {
        try {
            while (true) {
                Thread.sleep(random.nextInt(1000));
                String cake = "[ Cake No."+nextId()+" by "+getName()+" ]";
                table.put(cake);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static synchronized int nextId() {
        return id++;
    }
}

If the Table class chooses to use BlockingQueue in the juc package, it's very simple. Common BlockingQueues include Linked BlockingQueue based on linked list, Array BlockingQueue based on array, and Priority BlockingQueue with priority. Here we use a linked list-based BlockingQueue and rewrite the code as follows:

/**
 * @author koma <komazhang@foxmail.com>
 * @date 2018-10-18
 */
public class Table {
    private final LinkedBlockingQueue<String> queue;
    private final int count;

    public Table(int count) {
        this.count = count;
        queue = new LinkedBlockingQueue<>();
    }

    public synchronized void put(String cake) throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+" puts "+cake);
        queue.put(cake);
    }

    public synchronized String take() throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+" takes "+cake);
        return queue.take();
    }
}

Second, read-write lock mode

Read-write lock mode is a mode that considers read and write operations separately. In this mode, an example includes two types of locks: read and write locks. Write locks can be acquired by multiple threads at the same time, while read locks can only be acquired by one thread at the same time. Moreover, it is stipulated that read and write operations cannot be carried out, and write operations cannot be carried out.

In general, executing mutex processing can degrade program performance, but it can improve program performance if read-write operations are considered separately.

The following example code uses the read-write lock mode to implement the read-write operation on the Data class, the most critical of which is the ReadWriteLock class, which uses the immutable mode.

/**
 * @author koma <komazhang@foxmail.com>
 * @date 2018-10-18
 */
public class Main {
    public static void main(String[] args) {
        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();
    }
}

public class Data {
    private final char[] buffer;
    private final ReadWriteLock lock = new ReadWriteLock();

    public Data(int size) {
        this.buffer = new char[size];
        for (int i = 0; i < size; i++) {
            buffer[i] = '*';
        }
    }

    public char[] read() throws InterruptedException {
        try {
            lock.readLock();
            return doRead();
        } finally {
            lock.readUnlock();
        }
    }

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

    private void doWrite(char c) {
        for (int i = 0; i < buffer.length; i++) {
            buffer[i] = c;
            slowly();
        }
    }

    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 ReaderThread extends Thread {
    private final Data data;

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

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

public class WriterThread extends Thread {
    private final static Random random = new Random();
    private final Data data;
    private final String filler;
    private int index = 0;

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

    @Override
    public void run() {
        try {
            while (true) {
                char c = nextChar();
                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;
    }
}

public final class ReadWriteLock {
    private int readingReaders = 0; //Number of threads performing read operations
    private int waitingWriters = 0; //Number of threads waiting to write locks
    private int writingWriters = 0; //Number of threads executing write operations
    private boolean preferWriter = true; //Write-first or not

    public synchronized void readLock() throws InterruptedException {
        while (writingWriters > 0 || (preferWriter && waitingWriters > 0)) {
            wait();
        }

        readingReaders++;
    }

    public synchronized void readUnlock() {
        readingReaders--;
        preferWriter = true;
        notifyAll();
    }

    public synchronized void writeLock() throws InterruptedException {
        waitingWriters++;
        try {
            while (readingReaders > 0 || writingWriters > 0) {
                wait();
            }
        } finally {
            waitingWriters--;
        }
        writingWriters++;
    }

    public synchronized void writeUnlock() {
        writingWriters--;
        preferWriter = false;
        notifyAll();
    }
}

The read-write lock mode takes advantage of the fact that the read operation does not modify the instance state, so that there is no conflict between multiple read operation threads, so there is no need to do synchronization processing to provide program performance. But the performance improvement is not absolute. It needs to be measured in practice. At the same time, the following two scenarios need to be considered:

  • Reading is a time-consuming operation. When reading is simple, single-threaded mode is simpler and more efficient than single-threaded mode.
  • Reading frequency is higher than writing frequency. When writing frequency is higher, writing operation frequently interrupts reading operation, and the advantages of read-write lock mode are reduced.

1. The Meaning of Locks

synchronized is a lock used to obtain instances. Each instance of an object in Java holds a lock, but the same lock can only be held by one thread at the same time. This structure is prescribed by the Java specification, and so is the JVM. This lock is called a physical lock.

In read-write lock mode, the locks here are different from those acquired by synchronized. This is not a lock specified by the Java specification, but a lock implemented by developers themselves, which is called a logical lock.

ReadWriteLock classes have read locks and write locks, but these are logical locks, which share the same physical lock of the ReadWriteLock class instance acquired by synchronization. This is why we have declared synchronized keywords on lock and unlock methods in ReadWriteLock class, because they ultimately share the same physical lock, which can only be held by one thread at the same time, which guarantees the realization of logical lock.

2. Read-write locks in Java

ReadWriteLock interface and ReentrantReadWriteLock implementation class are provided in the juc package. The Data class code rewritten by ReentrantReadWriteLock is as follows:

/**
 * @author koma <komazhang@foxmail.com>
 * @date 2018-10-18
 */
public class Data {
    private final char[] buffer;
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    public Data(int size) {
        this.buffer = new char[size];
        for (int i = 0; i < size; i++) {
            buffer[i] = '*';
        }
    }

    public char[] read() throws InterruptedException {
        readLock.lock();
        try {
            Thread.sleep(10000);
            return doRead();
        } finally {
            readLock.unlock();
        }
    }

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

    //Other methods remain unchanged

Posted by salih0vicX on Wed, 30 Jan 2019 13:36:14 -0800