Merge pull request #326 from hoswey/master

implements the #75 reader writer lock
This commit is contained in:
Ilkka Seppälä 2016-01-12 20:15:22 +02:00
commit adf131b784
13 changed files with 685 additions and 0 deletions

View File

@ -62,6 +62,7 @@
<module>intercepting-filter</module>
<module>producer-consumer</module>
<module>poison-pill</module>
<module>reader-writer-lock</module>
<module>lazy-loading</module>
<module>service-layer</module>
<module>specification</module>

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<class-diagram version="1.1.8" icons="true" automaticImage="PNG" always-add-relationships="false" generalizations="true"
realizations="true" associations="true" dependencies="false" nesting-relationships="true">
<class id="1" language="java" name="com.iluwatar.reader.writer.lock.Writer" project="reader-writer-lock"
file="/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/Writer.java" binary="false"
corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="487" y="105"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="2" language="java" name="com.iluwatar.reader.writer.lock.ReaderWriterLock.WriteLock"
project="reader-writer-lock"
file="/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/ReaderWriterLock.java" binary="false"
corner="BOTTOM_RIGHT">
<position height="191" width="197" x="488" y="313"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="3" language="java" name="com.iluwatar.reader.writer.lock.ReaderWriterLock" project="reader-writer-lock"
file="/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/ReaderWriterLock.java" binary="false"
corner="BOTTOM_RIGHT">
<position height="245" width="224" x="606" y="28"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="4" language="java" name="com.iluwatar.reader.writer.lock.Reader" project="reader-writer-lock"
file="/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/Reader.java" binary="false"
corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="944" y="109"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="5" language="java" name="com.iluwatar.reader.writer.lock.ReaderWriterLock.ReadLock"
project="reader-writer-lock"
file="/reader-writer-lock/src/main/java/com/iluwatar/reader/writer/lock/ReaderWriterLock.java" binary="false"
corner="BOTTOM_RIGHT">
<position height="191" width="197" x="725" y="313"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<nesting id="6">
<end type="SOURCE" refId="3"/>
<end type="TARGET" refId="2"/>
</nesting>
<association id="7">
<end type="SOURCE" refId="3" navigable="false">
<attribute id="8" name="writerLock"/>
<multiplicity id="9" minimum="0" maximum="1"/>
</end>
<end type="TARGET" refId="2" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<association id="10">
<end type="SOURCE" refId="3" navigable="false">
<attribute id="11" name="readerLock"/>
<multiplicity id="12" minimum="0" maximum="1"/>
</end>
<end type="TARGET" refId="5" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<nesting id="13">
<end type="SOURCE" refId="3"/>
<end type="TARGET" refId="5"/>
</nesting>
<classifier-display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</classifier-display>
<association-display labels="true" multiplicity="true"/>
</class-diagram>

View File

@ -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**
* [Readerswriter lock](https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock)
* [Readerswriters_problem](https://en.wikipedia.org/wiki/Readers%E2%80%93writers_problem)

View File

@ -0,0 +1,24 @@
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.iluwatar</groupId>
<artifactId>java-design-patterns</artifactId>
<version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>reader-writer-lock</artifactId>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -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.
* <p>
* 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.
*
* <p>
* 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");
}
}
}

View File

@ -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");
}
}

View File

@ -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.
* <p>
* 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. <br>
* 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.
* <p>
* This is the most important field in this class to control the access for reader/writer.
*/
private Set<Object> 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();
}
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}
}

View File

@ -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");
}
}
}

View File

@ -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");
}
}
}