diff --git a/pom.xml b/pom.xml index 89a197347..d2b02fdf7 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,7 @@ intercepting-filter producer-consumer poison-pill + reader-writer-lock lazy-loading service-layer specification diff --git a/reader-writer-lock/etc/reader-writer-lock.png b/reader-writer-lock/etc/reader-writer-lock.png new file mode 100644 index 000000000..f7b600530 Binary files /dev/null and b/reader-writer-lock/etc/reader-writer-lock.png differ diff --git a/reader-writer-lock/etc/reader-writer-lock.ucls b/reader-writer-lock/etc/reader-writer-lock.ucls new file mode 100644 index 000000000..920904e76 --- /dev/null +++ b/reader-writer-lock/etc/reader-writer-lock.ucls @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/reader-writer-lock/index.md b/reader-writer-lock/index.md new file mode 100644 index 000000000..91d16892c --- /dev/null +++ b/reader-writer-lock/index.md @@ -0,0 +1,29 @@ +--- +layout: pattern +title: Reader Writer Lock +folder: reader-writer-lock +permalink: /patterns/reader-writer-lock/ +categories: Concurrent +tags: +- Java +--- + +**Intent:** + +Suppose we have a shared memory area with the basic constraints detailed above. It is possible to protect the shared data behind a mutual exclusion mutex, in which case no two threads can access the data at the same time. However, this solution is suboptimal, because it is possible that a reader R1 might have the lock, and then another reader R2 requests access. It would be foolish for R2 to wait until R1 was done before starting its own read operation; instead, R2 should start right away. This is the motivation for the Reader Writer Lock pattern. + +![alt text](./etc/reader-writer-lock.png "Reader writer lock") + +**Applicability:** + +Application need to increase the performance of resource synchronize for multiple thread, in particularly there are mixed read/write operations. + +**Real world examples:** + +* [Java Reader Writer Lock](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReadWriteLock.html) + +**Credits** + +* [Readers–writer lock](https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock) + +* [Readers–writers_problem](https://en.wikipedia.org/wiki/Readers%E2%80%93writers_problem) \ No newline at end of file diff --git a/reader-writer-lock/pom.xml b/reader-writer-lock/pom.xml new file mode 100644 index 000000000..14b17011d --- /dev/null +++ b/reader-writer-lock/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.10.0-SNAPSHOT + + reader-writer-lock + + + junit + junit + test + + + org.mockito + mockito-core + test + + + + diff --git a/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/App.java b/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/App.java new file mode 100644 index 000000000..1d02b6cad --- /dev/null +++ b/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/App.java @@ -0,0 +1,57 @@ +package com.iluwatar.reader.writer.lock; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +/** + * + * In a multiple thread applications, the threads may try to synchronize the shared resources + * regardless of read or write operation. It leads to a low performance especially in a "read more + * write less" system as indeed the read operations are thread-safe to another read operation. + *

+ * Reader writer lock is a synchronization primitive that try to resolve this problem. This pattern + * allows concurrent access for read-only operations, while write operations require exclusive + * access. This means that multiple threads can read the data in parallel but an exclusive lock is + * needed for writing or modifying data. When a writer is writing the data, all other writers or + * readers will be blocked until the writer is finished writing. + * + *

+ * This example use two mutex to demonstrate the concurrent access of multiple readers and + * writers. + * + * + * @author hongshuwei@gmail.com + */ +public class App { + + /** + * Program entry point + * + * @param args command line args + */ + public static void main(String[] args) { + + ExecutorService executeService = Executors.newFixedThreadPool(1000); + ReaderWriterLock lock = new ReaderWriterLock(); + + // Start 10 readers + IntStream.range(0, 10) + .forEach(i -> executeService.submit(new Reader("Reader " + i, lock.readLock()))); + + // Start 10 writers + IntStream.range(0, 10) + .forEach(i -> executeService.submit(new Writer("Writer " + i, lock.writeLock()))); + // In the system console, it can see that the read operations are executed concurrently while + // write operations are exclusive. + executeService.shutdown(); + try { + executeService.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + System.out.println("Error waiting for ExecutorService shutdown"); + } + + } + +} diff --git a/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/Reader.java b/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/Reader.java new file mode 100644 index 000000000..5704fdf28 --- /dev/null +++ b/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/Reader.java @@ -0,0 +1,40 @@ +package com.iluwatar.reader.writer.lock; + +import java.util.concurrent.locks.Lock; + +/** + * Reader class, read when it acquired the read lock + */ +public class Reader implements Runnable { + + private Lock readLock; + + private String name; + + public Reader(String name, Lock readLock) { + this.name = name; + this.readLock = readLock; + } + + @Override + public void run() { + readLock.lock(); + try { + read(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + readLock.unlock(); + } + } + + /** + * Simulate the read operation + * + */ + public void read() throws InterruptedException { + System.out.println(name + " begin"); + Thread.sleep(100); + System.out.println(name + " finish"); + } +} diff --git a/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/ReaderWriterLock.java b/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/ReaderWriterLock.java new file mode 100644 index 000000000..b7edd149c --- /dev/null +++ b/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/ReaderWriterLock.java @@ -0,0 +1,211 @@ +package com.iluwatar.reader.writer.lock; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; + +/** + * Class responsible for control the access for reader or writer + * + * Allows multiple readers to hold the lock at same time, but if any writer holds the lock then + * readers wait. If reader holds the lock then writer waits. This lock is not fair. + */ +public class ReaderWriterLock implements ReadWriteLock { + + + private Object readerMutex = new Object(); + + private int currentReaderCount = 0; + + /** + * Global mutex is used to indicate that whether reader or writer gets the lock in the moment. + *

+ * 1. When it contains the reference of {@link readerLock}, it means that the lock is acquired by + * the reader, another reader can also do the read operation concurrently.
+ * 2. When it contains the reference of reference of {@link writerLock}, it means that the lock is + * acquired by the writer exclusively, no more reader or writer can get the lock. + *

+ * This is the most important field in this class to control the access for reader/writer. + */ + private Set globalMutex = new HashSet<>(); + + private ReadLock readerLock = new ReadLock(); + private WriteLock writerLock = new WriteLock(); + + @Override + public Lock readLock() { + return readerLock; + } + + @Override + public Lock writeLock() { + return writerLock; + } + + /** + * return true when globalMutex hold the reference of writerLock + */ + private boolean doesWriterOwnThisLock() { + return globalMutex.contains(writerLock); + } + + /** + * return true when globalMutex hold the reference of readerLock + */ + private boolean doesReaderOwnThisLock() { + return globalMutex.contains(readerLock); + } + + /** + * Nobody get the lock when globalMutex contains nothing + * + */ + private boolean isLockFree() { + return globalMutex.isEmpty(); + } + + private void waitUninterruptibly(Object o) { + try { + o.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * Reader Lock, can be access for more than one reader concurrently if no writer get the lock + */ + private class ReadLock implements Lock { + + @Override + public void lock() { + + synchronized (readerMutex) { + + currentReaderCount++; + if (currentReaderCount == 1) { + // Try to get the globalMutex lock for the first reader + synchronized (globalMutex) { + while (true) { + // If the no one get the lock or the lock is locked by reader, just set the reference + // to the globalMutex to indicate that the lock is locked by Reader. + if (isLockFree() || doesReaderOwnThisLock()) { + globalMutex.add(this); + break; + } else { + // If lock is acquired by the write, let the thread wait until the writer release + // the lock + waitUninterruptibly(globalMutex); + } + } + } + + } + } + } + + @Override + public void unlock() { + + synchronized (readerMutex) { + currentReaderCount--; + // Release the lock only when it is the last reader, it is ensure that the lock is released + // when all reader is completely. + if (currentReaderCount == 0) { + synchronized (globalMutex) { + // Notify the waiter, mostly the writer + globalMutex.remove(this); + globalMutex.notifyAll(); + } + } + } + + } + + @Override + public void lockInterruptibly() throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean tryLock() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public Condition newCondition() { + throw new UnsupportedOperationException(); + } + + } + + /** + * Writer Lock, can only be accessed by one writer concurrently + */ + private class WriteLock implements Lock { + + @Override + public void lock() { + + synchronized (globalMutex) { + + while (true) { + // When there is no one acquired the lock, just put the writeLock reference to the + // globalMutex to indicate that the lock is acquired by one writer. + // It is ensure that writer can only get the lock when no reader/writer acquired the lock. + if (isLockFree()) { + globalMutex.add(this); + break; + } else if (doesWriterOwnThisLock()) { + // Wait when other writer get the lock + waitUninterruptibly(globalMutex); + } else if (doesReaderOwnThisLock()) { + // Wait when other reader get the lock + waitUninterruptibly(globalMutex); + } else { + throw new AssertionError("it should never reach here"); + } + } + } + } + + @Override + public void unlock() { + + synchronized (globalMutex) { + globalMutex.remove(this); + // Notify the waiter, other writer or reader + globalMutex.notifyAll(); + } + } + + @Override + public void lockInterruptibly() throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean tryLock() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public Condition newCondition() { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/Writer.java b/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/Writer.java new file mode 100644 index 000000000..d7afa385a --- /dev/null +++ b/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/Writer.java @@ -0,0 +1,40 @@ +package com.iluwatar.reader.writer.lock; + +import java.util.concurrent.locks.Lock; + +/** + * Writer class, write when it acquired the write lock + */ +public class Writer implements Runnable { + + private Lock writeLock = null; + + private String name; + + public Writer(String name, Lock writeLock) { + this.name = name; + this.writeLock = writeLock; + } + + + @Override + public void run() { + writeLock.lock(); + try { + write(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + writeLock.unlock(); + } + } + + /** + * Simulate the write operation + */ + public void write() throws InterruptedException { + System.out.println(name + " begin"); + Thread.sleep(100); + System.out.println(name + " finish"); + } +} diff --git a/reader-writer-lock/src/test/java/com/iluwatar/reader/writer/lock/AppTest.java b/reader-writer-lock/src/test/java/com/iluwatar/reader/writer/lock/AppTest.java new file mode 100644 index 000000000..5dd6feaab --- /dev/null +++ b/reader-writer-lock/src/test/java/com/iluwatar/reader/writer/lock/AppTest.java @@ -0,0 +1,16 @@ +package com.iluwatar.reader.writer.lock; + +import org.junit.Test; + +/** + * Application test + */ +public class AppTest { + + @Test + public void test() throws Exception { + String[] args = {}; + App.main(args); + + } +} diff --git a/reader-writer-lock/src/test/java/com/iluwatar/reader/writer/lock/ReaderAndWriterTest.java b/reader-writer-lock/src/test/java/com/iluwatar/reader/writer/lock/ReaderAndWriterTest.java new file mode 100644 index 000000000..b9516f3a7 --- /dev/null +++ b/reader-writer-lock/src/test/java/com/iluwatar/reader/writer/lock/ReaderAndWriterTest.java @@ -0,0 +1,84 @@ +package com.iluwatar.reader.writer.lock; + +import static org.mockito.Mockito.after; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * @author hongshuwei@gmail.com + */ +public class ReaderAndWriterTest { + + ExecutorService executeService; + + @Before + public void setup() { + executeService = Executors.newFixedThreadPool(2); + } + + /** + * Verify reader and writer can only get the lock to read and write orderly + */ + @Test + public void testReadAndWrite() throws Exception { + + ReaderWriterLock lock = new ReaderWriterLock(); + + Reader reader1 = spy(new Reader("Reader 1", lock.readLock())); + Writer writer1 = spy(new Writer("Writer 1", lock.writeLock())); + + executeService.submit(reader1); + // Let reader1 execute first + Thread.sleep(50); + executeService.submit(writer1); + + verify(reader1, timeout(99).atLeastOnce()).read(); + verify(writer1, after(10).never()).write(); + verify(writer1, timeout(100).atLeastOnce()).write(); + + } + + /** + * Verify reader and writer can only get the lock to read and write orderly + */ + @Test + public void testWriteAndRead() throws Exception { + + ExecutorService executeService = Executors.newFixedThreadPool(2); + ReaderWriterLock lock = new ReaderWriterLock(); + + Reader reader1 = spy(new Reader("Reader 1", lock.readLock())); + Writer writer1 = spy(new Writer("Writer 1", lock.writeLock())); + + executeService.submit(writer1); + // Let reader1 execute first + Thread.sleep(50); + executeService.submit(reader1); + + verify(writer1, timeout(99).atLeastOnce()).write(); + verify(reader1, after(10).never()).read(); + verify(reader1, timeout(100).atLeastOnce()).read(); + + + } + + @After + public void tearDown() { + executeService.shutdown(); + try { + executeService.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + System.out.println("Error waiting for ExecutorService shutdown"); + } + } +} + diff --git a/reader-writer-lock/src/test/java/com/iluwatar/reader/writer/lock/ReaderTest.java b/reader-writer-lock/src/test/java/com/iluwatar/reader/writer/lock/ReaderTest.java new file mode 100644 index 000000000..c18bc1b2b --- /dev/null +++ b/reader-writer-lock/src/test/java/com/iluwatar/reader/writer/lock/ReaderTest.java @@ -0,0 +1,45 @@ +package com.iluwatar.reader.writer.lock; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +/** + * @author hongshuwei@gmail.com + */ +public class ReaderTest { + + /** + * Verify that multiple readers can get the read lock concurrently + */ + @Test + public void testRead() throws Exception { + + ExecutorService executeService = Executors.newFixedThreadPool(2); + ReaderWriterLock lock = new ReaderWriterLock(); + + Reader reader1 = spy(new Reader("Reader 1", lock.readLock())); + Reader reader2 = spy(new Reader("Reader 2", lock.readLock())); + + executeService.submit(reader1); + executeService.submit(reader2); + + // Read operation will hold the read lock 100 milliseconds, so here we guarantee that each + // readers can read in 99 milliseconds to prove that multiple read can perform in the same time. + verify(reader1, timeout(99).atLeastOnce()).read(); + verify(reader2, timeout(99).atLeastOnce()).read(); + + executeService.shutdown(); + try { + executeService.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + System.out.println("Error waiting for ExecutorService shutdown"); + } + } +} diff --git a/reader-writer-lock/src/test/java/com/iluwatar/reader/writer/lock/WriterTest.java b/reader-writer-lock/src/test/java/com/iluwatar/reader/writer/lock/WriterTest.java new file mode 100644 index 000000000..c81a56dc1 --- /dev/null +++ b/reader-writer-lock/src/test/java/com/iluwatar/reader/writer/lock/WriterTest.java @@ -0,0 +1,52 @@ +package com.iluwatar.reader.writer.lock; + +import static org.mockito.Mockito.after; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +/** + * @author hongshuwei@gmail.com + */ +public class WriterTest { + + /** + * Verify that multiple writers will get the lock in order. + */ + @Test + public void testWrite() throws Exception { + + ExecutorService executeService = Executors.newFixedThreadPool(2); + ReaderWriterLock lock = new ReaderWriterLock(); + + Writer writer1 = spy(new Writer("Writer 1", lock.writeLock())); + Writer writer2 = spy(new Writer("Writer 2", lock.writeLock())); + + executeService.submit(writer1); + // Let write1 execute first + Thread.sleep(50); + executeService.submit(writer2); + + // Write operation will hold the write lock 100 milliseconds, so here we verify that when two + // write excute concurrently + // 1. The first write will get the lock and and write in 60ms + // 2. The second writer will cannot get the lock when first writer get the lock + // 3. The second writer will get the lock as last + verify(writer1, timeout(10).atLeastOnce()).write(); + verify(writer2, after(10).never()).write(); + verify(writer2, timeout(100).atLeastOnce()).write(); + + executeService.shutdown(); + try { + executeService.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + System.out.println("Error waiting for ExecutorService shutdown"); + } + } +}