diff --git a/reader-writer-lock/etc/reader-writer-lock.png b/reader-writer-lock/etc/reader-writer-lock.png index 06207b502..f7b600530 100644 Binary files a/reader-writer-lock/etc/reader-writer-lock.png 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 index 18ffb8ccb..920904e76 100644 --- a/reader-writer-lock/etc/reader-writer-lock.ucls +++ b/reader-writer-lock/etc/reader-writer-lock.ucls @@ -1,52 +1,52 @@ - + + + + + + + - + - - + - - + - - - - - - - - - + @@ -54,36 +54,28 @@ - - + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - + + + diff --git a/reader-writer-lock/index.md b/reader-writer-lock/index.md index 75f57a4cd..91d16892c 100644 --- a/reader-writer-lock/index.md +++ b/reader-writer-lock/index.md @@ -1,7 +1,7 @@ --- layout: pattern -title: Producer Consumer -folder: reader writer lock +title: Reader Writer Lock +folder: reader-writer-lock permalink: /patterns/reader-writer-lock/ categories: Concurrent tags: diff --git a/reader-writer-lock/pom.xml b/reader-writer-lock/pom.xml index f1553a4d1..14b17011d 100644 --- a/reader-writer-lock/pom.xml +++ b/reader-writer-lock/pom.xml @@ -1,18 +1,24 @@ - - 4.0.0 - - com.iluwatar - java-design-patterns - 1.9.0-SNAPSHOT - - reader-writer-lock - - - junit - junit - test - - + + 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 index aa36ef47c..1d02b6cad 100644 --- 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 @@ -1,24 +1,31 @@ package com.iluwatar.reader.writer.lock; -import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; /** - * Reader writer lock is a synchronization primitive that solves one of the readers–writers - * problems. An RW lock allows concurrent access for read-only operations, while write operations - * require exclusive access. + * + * 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. *

- * Below example use two mutexes to demonstrate the concurrent access of mutilple readers and + * 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 { - private static Random ran = new Random(); - /** * Program entry point * @@ -26,57 +33,25 @@ public class App { */ public static void main(String[] args) { - ExecutorService es = Executors.newFixedThreadPool(1000); + ExecutorService executeService = Executors.newFixedThreadPool(1000); ReaderWriterLock lock = new ReaderWriterLock(); - AtomicInteger index = new AtomicInteger(0); - IntStream.range(0, 100).forEach(i -> { - Runnable task = null; - if (ran.nextFloat() <= 0.6) { - task = new Runnable() { - @Override - public void run() { - Lock writeLock = lock.writeLock(); - writeLock.lock(); - try { - int cur = index.getAndIncrement(); - System.out.println("Writer " + cur + " begin"); - simulateReadOrWrite(); - System.out.println("Writer " + cur + " finish"); - } finally { - writeLock.unlock(); - } - } - }; - } else { - task = new Runnable() { + // Start 10 readers + IntStream.range(0, 10) + .forEach(i -> executeService.submit(new Reader("Reader " + i, lock.readLock()))); - @Override - public void run() { - Lock readLock = lock.readLock(); - readLock.lock(); - try { - int cur = index.getAndIncrement(); - System.out.println("Reader " + cur + " begin"); - simulateReadOrWrite(); - System.out.println("Reader " + cur + " finish"); - - } finally { - readLock.unlock(); - } - } - }; - } - es.submit(task); - }); - - } - - private static void simulateReadOrWrite() { + // 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 { - Thread.sleep((long) (ran.nextFloat() * 10)); + executeService.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { - e.printStackTrace(); + System.out.println("Error waiting for ExecutorService shutdown"); } + } + } diff --git a/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/Lock.java b/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/Lock.java deleted file mode 100644 index 7cbe47749..000000000 --- a/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/Lock.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.iluwatar.reader.writer.lock; - -/** - * Lock interface - */ -public interface Lock { - - /** - * Try to lock, it will wait until get the lock - */ - public void lock(); - - /** - * Release lock - */ - public void unlock(); -} - 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 index 2d92696e3..b7edd149c 100644 --- 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 @@ -2,62 +2,103 @@ 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 { +public class ReaderWriterLock implements ReadWriteLock { + + + private Object readerMutex = new Object(); + + private int currentReaderCount = 0; /** - * Mutex for reader + * 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 Object r = new Object(); - - /** - * Global mutex for reader or writer, use to save the holding object - */ - private Set g = new HashSet<>(); - - /** - * Current reader count - */ - private int readerCount = 0; - - private ReaderLock readLock = new ReaderLock(); - private WriterLock writeLock = new WriterLock(); + private Set globalMutex = new HashSet<>(); + private ReadLock readerLock = new ReadLock(); + private WriteLock writerLock = new WriteLock(); + @Override public Lock readLock() { - return readLock; + return readerLock; } - + @Override public Lock writeLock() { - return 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 ReaderLock implements Lock { + private class ReadLock implements Lock { @Override public void lock() { - synchronized (r) { - - readerCount++; - if (readerCount == 1) { - - synchronized (g) { + synchronized (readerMutex) { + currentReaderCount++; + if (currentReaderCount == 1) { + // Try to get the globalMutex lock for the first reader + synchronized (globalMutex) { while (true) { - if (isLockFree() || isReaderOwnThisLock()) { - g.add(this); + // 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 { - waitUninterruptely(g); + // If lock is acquired by the write, let the thread wait until the writer release + // the lock + waitUninterruptibly(globalMutex); } } } @@ -66,82 +107,105 @@ public class ReaderWriterLock { } } - @Override public void unlock() { - synchronized (r) { - readerCount--; - if (readerCount == 0) { - synchronized (g) { - g.remove(this); - g.notifyAll(); + 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 WriterLock implements Lock { + private class WriteLock implements Lock { @Override public void lock() { - synchronized (g) { + 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()) { - g.add(this); + globalMutex.add(this); break; - } else if (isWriterOwnThisLock()) { - waitUninterruptely(g); - } else if (isReaderOwnThisLock()) { - waitUninterruptely(g); + } 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 RuntimeException("it should never reach here"); + throw new AssertionError("it should never reach here"); } } } } - @Override public void unlock() { - synchronized (g) { - g.remove(this); - g.notifyAll(); + synchronized (globalMutex) { + globalMutex.remove(this); + // Notify the waiter, other writer or reader + globalMutex.notifyAll(); } } - } - private boolean isWriterOwnThisLock() { - return g.contains(writeLock); - } + @Override + public void lockInterruptibly() throws InterruptedException { + throw new UnsupportedOperationException(); + } - private boolean isReaderOwnThisLock() { - return g.contains(readLock); - } + @Override + public boolean tryLock() { + throw new UnsupportedOperationException(); + } - private boolean isLockFree() { - return g.isEmpty(); - } + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException(); + } - - - private void waitUninterruptely(Object o) { - try { - o.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); + @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 index 95656b14e..5dd6feaab 100644 --- 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 @@ -2,12 +2,8 @@ package com.iluwatar.reader.writer.lock; import org.junit.Test; -import com.iluwatar.reader.writer.lock.App; - /** - * * Application test - * */ public class AppTest { 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"); + } + } +}