implements the #75 reader writer lock, fix the problem of review

This commit is contained in:
hoswey
2016-01-10 21:23:52 +08:00
parent 2f84369003
commit 77d45c35e0
13 changed files with 482 additions and 206 deletions

View File

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

View File

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

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

@ -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.
* <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 Object r = new Object();
/**
* Global mutex for reader or writer, use to save the holding object
*/
private Set<Object> g = new HashSet<>();
/**
* Current reader count
*/
private int readerCount = 0;
private ReaderLock readLock = new ReaderLock();
private WriterLock writeLock = new WriterLock();
private Set<Object> 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();
}
}
}
}

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

@ -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 {

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