Locks In Java — Part 6.1 [ Stamped Lock , Optimistic Reads and Pessimistic Reads]

Avinashsoni
7 min readMay 7, 2024

--

Hello in this article we are going to learn about stamped locks in java

Stamped Lock are a advanced version of locking in java where it comes with several features which are not available in other locking systems we have discussed earlier in this series

Some of the Handy Features of stamped locks are :

  1. Avoid starvation ( the problem with ReadWrite Reentrant Locks)
  2. Optimistic Read Support
  3. Allows Conversion of Read Lock into Write Lock and Vice Versa

Stamped Lock are very useful in read-heavy systems where frequency of reads is much higher than the writes . Also we should make a note that these locks are not reentrant so at a same time more than one thread cannot acquire the lock.

with this little background on what stamped locks are lets start deep diving into aspects of it .

Modes of Stamped Lock

Stamped Lock work under three modes , namely :

  1. Read Mode
  2. Write Mode
  3. Optimistic Read Mode

Read and Write are pretty similar to what we have been doing so far but the advancement here is with the Optimistic Read Mode ( where earlier locks only supported Pessimistic Read )

Lets First Understand What are Optimistic Reads and What are Pessimistic Reads

Pessimistic Read

in pessimistic read it cannot read the shared resource if a writer thread has acquired the lock to it. basically pessimistic locking ensures that consistency should be there so if a thread is writing to a shared resource then reader thread will wait for the writer thread to complete its operation and release the lock and then reader thread will get access .

package com.example.multithreading.stampedLock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class PessimisticReadExample {
private int data = 0;
private Lock lock = new ReentrantLock();


public void writeData(int newValue) throws InterruptedException {
lock.lock();
System.out.println("Writer Thread Lock Acquired");
try{
data = newValue;
System.out.println("Writer Thread Operation Completed");
Thread.sleep(2000);
}finally {
lock.unlock();
System.out.println("Writer Thread Released Lock");
}


}

public void readData(){
lock.lock();
System.out.println("Reader thread lock acquired");
try {
System.out.println("read value = "+ data);
System.out.println("Reader thread Operation completed");

}finally {
lock.unlock();
System.out.println("Reader Thread Released Lock");
}
}


public static void main(String[] args) {
PessimisticReadExample example = new PessimisticReadExample();
// Creating the writer thread

Thread writerThread = new Thread(() -> {
try {
example.writeData(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});

Thread readerThread = new Thread(example::readData);

writerThread.start(); // writer thread has delay of 2s

try {
Thread.sleep(1000); // do a delay of 1s and see if the reader thread is able to read the value
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

readerThread.start();

}
}

Here is the output :

Clearly Here we can see that untill the writer thread complete the operation we are not allowing the reader thread to enter the shared resource . due to this blocking of reader thread often this results into starvation of thread.

Also there are very low chances that there will be conflicting scenario between write and read so we can move on to optmistic reads where they allow the reader thread to enter the shared resource even when the writer thread is running its operation ( provided if we have conflicts then we do have mechanisms to handle those issue ) [ all of this depend on our use case what we want to use ]

Optimistic Read :

package com.example.multithreading.stampedLock;

import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticReadExample {
private AtomicInteger data = new AtomicInteger(0);


public void writeData(int newValue) throws InterruptedException {
data.set(newValue);
System.out.println("Writer Thread Wrote Some Data ");

try {
Thread.sleep(2000);
} finally {

System.out.println("Writer Thread Completed Operation");
}


}

public void readData() {
System.out.println("read value = " + data);

}

public static void main(String[] args) {
OptimisticReadExample example = new OptimisticReadExample();
// Creating the writer thread

Thread writerThread = new Thread(() -> {
try {
example.writeData(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});

Thread readerThread = new Thread(example::readData);

writerThread.start(); // writer thread has delay of 2s

try {
Thread.sleep(1000); // do a delay of 1s and see if the reader thread is able to read the value
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

readerThread.start();

}


}

Here if you see we have used AtomicInteger to represent the shared resource ( this has methods like compare and set) which allow the user to handle conflict scenario .

here also until the writer thread is completing its operation we are not blocking the reader thread to enter .

hence along with conflict resolution methods optimistic reads can help a lot to enhance user experience.

with this idea now lets see what stamped lock has in the box for us to explore.

Stamp

unlike the locking mechanisms we have seen before stamped lock return a long value when the lock methods are called on it . this value is used to do the unlock operation .

Here is what written about stamped lock in java class

Stamps use finite representations, and are not cryptographically secure (i.e., a valid stamp may be guessable). Stamp values may recycle after (no sooner than) one year of continuous operation. A stamp held without use or validation for longer than this period may fail to validate correctly.

state representations

now here these values are used to determine the mode in which the stamp is acquired.

for this the logic is

calculate m = stamp & ABITS

now if

  1. m == WBIT -> write mode
  2. m == 0L -> optimistic read mode
  3. m > 0L and m ≤ RFULL -> read mode

so these checks are used to determine what is the current stage as per the stamp value released during the read and write locking process.

state

stamped lock have a state ( long ) value which it is initialised with when the constructor is created for stampedLock .

initially the value is = WBIT << 1

lets have a look now in the methods stamped lock provides

WRITE

  • tryAcquireWrite
  • unlockWriteState
  • releaseWrite
  • writeLock
  • tryWriteLock — have a timed version as well
  • writeLockInterruptibly

READ

  • tryAcquireRead
  • readLock
  • tryReadLock — have a timed version as well
  • readLockInterruptibly
  • tryOptimisticRead

Now lets explore how we can use read and write lock in stamped lock along with optimistic reads and then will start looking into how conversion of locks happens in stamped lock.

Generally Write Locks Follow the Given Convention

and Read Follow :

so when doing reads we follow this pattern

  1. create stamp for optimistic read
  2. perform operations -> keep it as minimum as possible as distance between optimistic stamp creation and validation should be minimum possible so that validation is successful as otherwise if some write thread acquire it and we are not in a situation to to optimistic read we wont get the advantage as the validation will fail
optimisticRead stamp creation logic
validation logic

based on the state of the lock and variables we saw earlier these validation logic is written

Here this loadFence is a internal method that is used,

fence in general are a memory barrier which are used for ordering of the memory operations in the threads.

there are two type of fence :

Load Fence / Read Barrier : this ensures that all the memory reads before the fence are completed before all the reads /writes after the fence.

Store Fence / Write Barrier : this ensures that all the memory writes before the fence are completed before all the reads / writes after the fence.

Usage :

volatile keywords for read / writes create load fence

synchronised keyword / locks create both the fences

3. if validations is a success then we are directly sending the read value without going to acquire new read lock and unlock ( in this it will wait for the write lock to release if it is held )

4. if validation fails then we will go by the book and will do the pessimistic read.

Preface of Next Article

we have these methods that allow conversion of a lock into different lock

  1. tryConvertToWriteLock(long stamp)
  2. tryConvertToReadLock(long stamp)
  3. tryConvertToOptimisticRead(long stamp)

Along with this we have other methods like

  1. getReadLockCount
  2. isWriteLocked
  3. isReadLocked
  4. isWriteLockStamp
  5. isReadLockStamp
  6. isLockStamp
  7. isOptimisticReadStamp

and we have ReadLockView and WriteLockView

all these sections will be covered in the part- 2 of stamped lock .

thanks for reading ( will share the GitHub repo in the second part)

--

--

Avinashsoni

SDE@BNY MELLON | Spring Boot | Learning and Growing EveryDay 😁 Linkedln 👇: https://www.linkedin.com/in/asoni93/