Locks In Java[Part 5.2] — Reentrant Locks

Avinashsoni
6 min readMar 16, 2024

--

In this Article we will cover the NonFairSync and FairSyn Classes which are based out of the implementation of the Sync Class we discussed in the first part of this : https://medium.com/@avinashsoni9829/locks-in-java-part-5-1-reentrant-locks-in-java-sync-class-bf11e8765507

First of All before learning any of these lets discuss what is meant by Fair or Non Fair here.

Fairness Parameter

In Reentrant Locks in the constructor we have an optional parameter for fairness.

basically what fairness parameter does is that it give the longest waiting thread the chance to acquire the lock first before any other thread.

to see lets take an example.

we have three threads thread -1 , thread — 2 , thread — 3 and have a lock.

all three threads are trying to acquire the lock ( using tryAcquire method)

Acquring of lock

[ sorry for bad drawing]

so in this scenario let assume that thread — 1 acquire the lock so what happens ( as per the implementation of AQS ) is that the other two threads are in the waiting queue now

waiting queue with locks

now if the lock is a fair lock then if a new thread — 4 try to acquire the lock then it will see if the lock has queuedPredecessors ( another method in AQS) and will not be able to get the lock. for non fair lock this thread can try to ignore the queue and get the lock and in such case the waiting threads will have to wait further to get the locks.

usually for fair scenario when the thread — 1 releases the lock it will call the unparkSuccessor method and that will try to awake the successor thread from the waiting queue ( that is thread — 2 ).

awakening scenario

so that how fairness parameter works.

now lets see when we should use the fairness parameter and which is better fair lock or unfair lock ?

When to use Fairness Parameter?

where sequence of thread matter we should use this, for example in banking transactions we want to follow the sequence of threads jobs to be done.

now we should also take care that when we use this fairness parameter it will cause a overhead of maintaining the sequence of order of execution so it will cause the performance issues.

why unfair is better ?

consider the scenario where there is heavy contention ( under heavy load) we have threads trying to acquire the lock .

lets again have a scenario where we have 3 threads t-1, t-2,t-3 trying to acquire the lock and t-1 gets the lock so t-2 and t-3 are waiting , now when some other thread comes to acquire the lock ( lets say t-4) then under heavy contention if wakening up of the next thread will take time and then it will acquire in between this the incoming thread ( t-4) can come and acquire the lock quickly ( considering awkening of the thread becomes a heavy operation considering the load on the system) so in such scneario there is no waiting for the thread and here t-4 can perform its job and release the thread untill t-2 is trying to wake up and then it can perform its operation.

so the choice of choosing fair and unfair scenario completely depends on our use case and level of performance and concurrency we want to achieve.

now with the background of fair and non fair sceanrio lets see the implementation of fairSync and NonFairSync classes which extends the sync class.

NonFairSync

it extends the Sync classes and only the initialTryLock() and tryAcquire(int acquire) methods are implemented separately.

initialTryLock()

final boolean initialTryLock() {
Thread current = Thread.currentThread();
if (compareAndSetState(0, 1)) { // first attempt is unguarded
setExclusiveOwnerThread(current);
return true;
} else if (getExclusiveOwnerThread() == current) {
int c = getState() + 1;
if (c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
} else
return false;
}

this is pretty similar to the implementation of the initialTryLock() method in the Sync Class.

one thing to note here is that we are doing int c = getState() + 1 instead of ++c for overflow check.

the reason being that for NonFairSync the incoming thread doesn’t need to be a part of the queue and it can cut the queue and get the thread so we need to explicitly get the current state and then add it to check for overflow whereas for the fair sync we can do ++c as we need to queue it in case the lock is not available.

tryAcquire()

protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}

here since it is a non fair sync we just check if it is free and then apply the CAS operation.

FairSync

this class also extends the Sync class and we rewrite the initialTryLock() and tryAcquire() methods.

initialTryLock()

final boolean initialTryLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) {
if (++c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
}
return false;
}

here acquisition is only possible when the queue is empty or the lock is not acquired.

so we get the state and we check if it doesn’t have queued threads and CAS is allowed then we set the thread as the current exlclusive thread.

tryAcquire()

protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && !hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}

here as discussed already if the current state = 0 ( lock is not acquired) and there are no queued predecessors then we can set the thread as the owner of the lock.

now we have seen about the fair and non fair sync then lets cover some remaning methods and facts about the reentrant class in java to conclude the implmentation.

Constructor

the reentrantLock constructor is initialzed with sync = NonFairSync() which means by default all the reentrant lock apply the nonFair strategy.
we also have a method to accept a boolean parameter for defining sync and nonSync reentrantLock

lock()

it internally uses the sync.lock()

here if the lock is already acquired by the same thread it increases the hold count.

lockInterruptibly()

this method again internally implments the sync lockInterruptibly method

here other thread can also acquire the lock if the interrupt method is called on the present lock owner.

some other method which directly calls there sync implementations are.

tryLock()

tryLock(long timeout,TimeUnit unit)

unlock()

newCondition()

getHoldCount()

isHeldByCurrentThread()

isLocked()

getOwner()

hasQueuedThreads()

hasQueuedThread(Thread t)

getQueueLength()

getQueuedThreads()

hasWaiters()

getWaitQueueLength()

getWaitingThreads()

toString()

all these methods are already part of sync class and some of them are from AQS which help us to create the locks.

this complete the reentrant lock , here we saw how interally reentrant lock uses Sync class which in turn is an implementation of AQS which uses waiting queue . Although we didn't deep dived into the implementation of AQS as it is very complex and can be taken as per choice of deep diving into these later on.

Thanks For Reading!! 😃

--

--

Avinashsoni

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