A Look at StampedLock
Java's StampedLock provides a means of optimistic locking for your programs. If you're careful to avoid deadlocking, it can make your work more concurrent-happy.
Join the DZone community and get the full member experience.
Join For FreeStampedLock was introduced in Java 8 and has been one of the most important features of the concurrent family.
This is to support the Read/Write Lock, as RWLock got introduced way back in Java 5, which is facing severe starvation.
Sometimes, we need better control over synchronization and to achieve this, we need a separate lock for read and write access. Thankfully, Java introduced ReentrantReadWriteLock under the java.util.concurrent.locks package, which provides an explicit locking mechanism.
In ReadWrite Locking policy, it allows the read lock to be held simultaneously by multiple reader threads, as long as there are no writers, and the write lock is exclusive.
But it was later discovered that ReentrantReadWriteLock has some severe issues with starvation if not handled properly (using fairness may help, but it may be an overhead and compromise throughput). For example, a number of reads but very few writes can cause the writer thread to fall into starvation. Make sure to analyze your setup properly to know how many reads/writes are present before choosing ReadWriteLock.
But it's within this ReadWriteLock series that Java introduced StampedLock.
StampedLock is made of a stamp and mode, where your lock acquisition method returns a stamp, which is a long value used for unlocking within the finally block. If the stamp is ever zero, that means there's been a failure to acquire access. StampedLock is all about giving us a possibility to perform optimistic reads.
Keep one thing in mind: StampedLock is not reentrant, so each call to acquire the lock always returns a new stamp and blocks if there's no lock available, even if the same thread already holds a lock, which may lead to deadlock.
Another point to note is that ReadWriteLock has two modes for controlling the read/write access while StampedLock has three modes of access:
Reading: Method 'public long readLock()' actually acquires a non-exclusive lock, and it blocks, if necessary, until available. It returns a stamp that can be used to unlock or convert the mode.
Writing: Method 'public long writeLock()' acquires an exclusive lock, and it blocks, if necessary, until available. It returns a stamp that can be used to unlock or convert the mode.
Optimistic reading: Method 'public long tryOptimisticRead()' acquires a non-exclusive lock without blocking only when it returns a stamp that can be later validated. Otherwise the value is zero if it doesn't acquire a lock. This is to allow read operations. After calling the tryOptimisticRead() method, always check if the stamp is valid using the 'lock.validate(stamp)' method, as the optimistic read lock doesn't prevent another thread from getting a write lock, which will make the optimistic read lock stamp's invalid.
You might be wondering how to convert the mode and when. Well:
There could be a situation when you acquired the write lock and written something and you wanted to read in the same critical section. So, as to not break the potential concurrent access, we can use the 'tryConvertToReadLock(long stamp)' method to acquire read access.
Now suppose you acquired the read lock, and after a successful read, you wanted to change the value. To do so, you need a write lock, which you can acquire using the 'tryConvertToWriteLock(long stamp)' method.
Note: One thing to note is that the tryConvertToReadLock and tryConvertToWriteLock methods will not block and may return the stamp as zero, which means these methods' calls were not successful.
With all that out of the way, let's run through an example of a simple tax office program where totalRevenue is an invariant.
IncomeTaxDept.java
package com.test.stampedlock;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @author arun.pandey
*/
public class IncomeTaxDept {
public static final Log LOG = LogFactory.getLog(IncomeTaxDept.class);
private List<TaxPayer> taxPayersList;
private double totalRevenue;
private final StampedLock sl = new StampedLock();
public IncomeTaxDept(long revenue, int numberOfTaxPayers) {
this.totalRevenue = revenue;
taxPayersList = new ArrayList<TaxPayer>(numberOfTaxPayers);
}
/**
* This method is to show the feature of writeLock() method
*/
public void payTax(TaxPayer taxPayer) {
double taxAmount = taxPayer.getTaxAmount();
long stamp = sl.writeLock();
try {
totalRevenue += taxAmount;
LOG.info(taxPayer.getTaxPayerName() + " paid tax, now Total Revenue --->>> "+ this.totalRevenue);
} finally {
sl.unlockWrite(stamp);
}
}
/**
* This method is to show the feature of writeLock() method
*/
public double getFederalTaxReturn(TaxPayer taxPayer) {
double incomeTaxRetunAmount = taxPayer.getTaxAmount() * 10/100;
long stamp = sl.writeLock();
try {
this.totalRevenue -= incomeTaxRetunAmount;
}
finally {
sl.unlockWrite(stamp);
}
return incomeTaxRetunAmount;
}
/**
* This method is to show the feature of readLock() method
*/
public double getTotalRevenue() {
long stamp = sl.readLock();
try {
return this.totalRevenue;
} finally {
sl.unlockRead(stamp);
}
}
/**
* This method is to show the feature of tryOptimisticRead() method
*/
public double getTotalRevenueOptimisticRead() {
long stamp = sl.tryOptimisticRead();
double balance = this.totalRevenue;
//calling validate(stamp) method to ensure that stamp is valid, if not then acquiring the read lock
if (!sl.validate(stamp)){
LOG.info("stamp for tryOptimisticRead() is not valid now, so acquiring the read lock");
stamp = sl.readLock();
}
try {
balance = this.totalRevenue;
} finally {
sl.unlockRead(stamp);
}
return balance;
}
/**
* This method is to show the feature of tryConvertToWriteLock() method
*/
public double getStateTaxReturnUisngConvertToWriteLock(TaxPayer taxPayer) {
double incomeTaxRetunAmount = taxPayer.getTaxAmount() * 5/100;
long stamp = sl.readLock();
//Trying to upgrade the lock from read to write
stamp = sl.tryConvertToWriteLock(stamp);
//Checking if tryConvertToWriteLock got success otherwise call writeLock method
if(stamp == 0L){
LOG.info("stamp is zero for tryConvertToWriteLock(), so acquiring the write lock");
stamp = sl.writeLock();
}
try {
this.totalRevenue -= incomeTaxRetunAmount;
} finally {
sl.unlockWrite(stamp);
}
return incomeTaxRetunAmount;
}
public void registerTaxPayer(TaxPayer taxPayer){
taxPayersList.add(taxPayer);
}
public List<TaxPayer> getTaxPayersList() {
return taxPayersList;
}
}
TaxPayer.java
package com.test.stampedlock;
/**
* @author arun.pandey
*/
public class TaxPayer {
private String taxPayerName;
private String taxPayerSsn;
private double taxAmount;
public String getTaxPayerName() {
return taxPayerName;
}
public void setTaxPayerName(String taxPayerName) {
this.taxPayerName = taxPayerName;
}
public String getTaxPayerSsn() {
return taxPayerSsn;
}
public void setTaxPayerSsn(String taxPayerSsn) {
this.taxPayerSsn = taxPayerSsn;
}
public double getTaxAmount() {
return taxAmount;
}
public void setTaxAmount(double taxAmount) {
this.taxAmount = taxAmount;
}
}
IncomeTaxClient.java
package com.test.stampedlock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @author arun.pandey
*/
public class IncomeTaxClient {
public static final Log LOG = LogFactory.getLog(IncomeTaxClient.class);
private static final int NUMBER_OF_TAX_PAYER = 1000;
private static IncomeTaxDept incomeTaxDept = new IncomeTaxDept(1000, NUMBER_OF_TAX_PAYER);
public static void main(String[] args) {
registerTaxPayers();
ExecutorService executor = Executors.newFixedThreadPool(30);
LOG.info("Initial IncomeTax Department's total revenue is ===>> "+incomeTaxDept.getTotalRevenue());
for(TaxPayer taxPayer : incomeTaxDept.getTaxPayersList())
executor.submit(() -> {
try {
Thread.sleep(100);
incomeTaxDept.payTax(taxPayer);
double revenue = incomeTaxDept.getTotalRevenue();
LOG.info("IncomeTax Department's total revenue after tax paid at client code is --->> " +revenue);
double returnAmount = incomeTaxDept.getFederalTaxReturn(taxPayer);
LOG.info(taxPayer.getTaxPayerName() + " received the Federal return amount = " +returnAmount);
revenue = incomeTaxDept.getTotalRevenueOptimisticRead();
LOG.info("IncomeTax Department's total revenue after getting Federal tax return at client code is --->> " +revenue);
double stateReturnAmount = incomeTaxDept.getStateTaxReturnUisngConvertToWriteLock(taxPayer);
LOG.info(taxPayer.getTaxPayerName() + " received the State tax return amount = " +stateReturnAmount);
revenue = incomeTaxDept.getTotalRevenueOptimisticRead();
LOG.info("IncomeTax Department's total revenue after getting State tax return at client code is --->> " +revenue);
Thread.sleep(100);
} catch (Exception e) {}
}
);
executor.shutdown();
}
private static void registerTaxPayers(){
for(int i = 1; i < NUMBER_OF_TAX_PAYER+1; i++){
TaxPayer taxPayer = new TaxPayer();
taxPayer.setTaxAmount(2000);
taxPayer.setTaxPayerName("Payer-"+i);
taxPayer.setTaxPayerSsn("xx-xxx-xxxx"+i);
incomeTaxDept.registerTaxPayer(taxPayer);
}
}
}
Now, let's run the IncomeTaxClient code to see the behavior of StampedLock's implementation:
Now, I have increased the NUMBER_OF_TAX_PAYER in the client code to 1000 and the thread pool count to 8. It quickly fell into a deadlock:
As I said before, StampedLock is not reentrant, hence it went into a deadlock situation. So, be careful when you design your StampedLock implementation.
I hope this helped you understand StampedLock in a better way. Happy learning!
Opinions expressed by DZone contributors are their own.
Comments