224. StampedLock

In a nutshell, StampedLock performs better than ReentrantReadWriteLock and supports optimistic reads. It is not like reentrant; therefore, it is prone to deadlocks. Mainly, a lock acquisition returns a stamp (a long value) that it is used in the finally block for unlocking. Each attempt to acquire a lock results in a new stamp, and, if no lock is available, then it may block until available. In other words, if the current thread is holding the lock, and attempts to acquire the lock again, it may cause a deadlock.

The StampedLock read/write orchestration process is achieved via several methods as follows:

  • readLock(): Non-exclusively acquires the lock, blocking if necessary, until available. For a non-blocking attempt of acquiring the read lock, we have to tryReadLock(). For timeout blocking, we have tryReadLock​(long time, TimeUnit unit). The returned stamp is used in unlockRead().

  • writeLock(): Exclusively acquires the lock, blocking if necessary until available. For a non-blocking attempt to acquire the write lock, we have tryWriteLock(). For timeout blocking, we have tryWriteLock​(long time, TimeUnit unit). The returned stamp is used in unlockWrite().

  • tryOptimisticRead(): This is the method that adds a big plus to StampedLock. This method returns a stamp that should be validated via the validate​() flag method. If the lock is not currently held in write mode, then the returned stamp is non-zero only.

The idioms for readLock() and writeLock() are pretty straightforward:

StampedLock lock = new StampedLock();
...
long stamp = lock.readLock() / writeLock();

try {
...
} finally {
lock.unlockRead(stamp) / unlockWrite(stamp);
}

An attempt to give an idiom for tryOptimisticRead() can result in the following:

StampedLock lock = new StampedLock();

int x; // a writer-thread can modify x
...
long stamp = lock.tryOptimisticRead();
int thex = x;

if (!lock.validate(stamp)) {
stamp = lock.readLock();

try {
thex = x;
} finally {
lock.unlockRead(stamp);
}
}

return thex;

In this idiom, notice that the initial value (x) is assigned to the thex variable after getting the optimistic read lock. Then the validate() flag method is used to validate that the stamped lock has not been exclusively acquired since the emittance of the given stamp. If validate() returns false (equivalent with the fact that the write lock is acquired by a thread after the optimistic lock is acquired), then the read lock is acquired via the blocking readLock() and the value (x) is assigned again. Keep in mind that, if there is any write lock, the read lock may block.  Acquiring the optimistic lock allows us to read the value(s) and then verify if there is any change in these value(s). Only if there is, will we have to go through the blocking read lock.

The following code represents a StampedLock usage case that reads and writes an integer amount variable. Basically, we reiterate the solution from the previous problem via optimistic reads:

public class ReadWriteWithStampedLock {

private static final Logger logger
= Logger.getLogger(ReadWriteWithStampedLock.class.getName());
private static final Random rnd = new Random();

private static final StampedLock lock = new StampedLock();

private static final OptimisticReader optimisticReader
= new OptimisticReader();
private static final Writer writer = new Writer();

private static int amount;

private static class OptimisticReader implements Runnable {

@Override
public void run() {
long stamp = lock.tryOptimisticRead();

// if the stamp for tryOptimisticRead() is not valid
// then the thread attempts to acquire a read lock
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
logger.info(() -> "Read amount (read lock): " + amount
+ " by " + Thread.currentThread().getName());
} finally {
lock.unlockRead(stamp);
}
} else {
logger.info(() -> "Read amount (optimistic read): " + amount
+ " by " + Thread.currentThread().getName());
}
}
}

private static class Writer implements Runnable {

@Override
public void run() {

long stamp = lock.writeLock();

try {
Thread.sleep(rnd.nextInt(2000));
logger.info(() -> "Increase amount with 10 by "
+ Thread.currentThread().getName());

amount += 10;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
logger.severe(() -> "Exception: " + ex);
} finally {
lock.unlockWrite(stamp);
}
}
}
...
}

And, let's perform 10 reads and 10 writes with two readers and four writers:

ExecutorService readerService = Executors.newFixedThreadPool(2);
ExecutorService writerService = Executors.newFixedThreadPool(4);

for (int i = 0; i < 10; i++) {
readerService.execute(optimisticReader);
writerService.execute(writer);
}

A possible output will be the following:

...
[12:12:07] [INFO] Increase amount with 10 by pool-2-thread-4
[12:12:07] [INFO] Read amount (read lock): 90 by pool-1-thread-2
[12:12:07] [INFO] Read amount (optimistic read): 90 by pool-1-thread-2
[12:12:07] [INFO] Increase amount with 10 by pool-2-thread-1
...

Starting with JDK 10, we can query the type of a stamp using isWriteLockStamp(), isReadLockStamp(), isLockStamp(), and isOptimisticReadStamp(). Based on the type, we can decide the proper unlock method, for example, as follows:

if (StampedLock.isReadLockStamp(stamp))
lock.unlockRead(stamp);
}

In the code bundled to this book, there is also an application for exemplifying the tryConvertToWriteLock​() method. In addition, you may be interested in developing applications that use tryConvertToReadLock​() and tryConvertToOptimisticRead().

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.136.142