Java Concurrency: Locks
By: Dmytro Timchenko
Tags: Software Development, Technology, Software Engineering, Java, Programming

A lock is a thread synchronization mechanism like synchronized blocks except locks can be more sophisticated than Java’s synchronized blocks. Locks are created using synchronized blocks, so it is not like we can get totally rid of the synchronized keyword.

From Java 5 the package java.util.concurrent.locks contains several lock implementations, so you may not have to implement your own locks. But you will still need to know how to use them, and it can still be useful to know the theory behind their implementation.

Lock vs Synchronized Block

There are few differences between the use of synchronized block and using Lock API:

A synchronized block is fully contained within a method — we can have Lock API’s lock() and unlock() operation in separate methods

we can have Lock API’s lock() and unlock() operation in separate methods A synchronized block doesn’t support fairness, any thread can acquire the lock once released, no preference can be specified. We can achieve fairness within the Lock APIs by specifying the fairness property . It makes sure that the longest waiting thread is given access to the lock

. It makes sure that the longest waiting thread is given access to the lock A thread gets blocked if it can’t get access to the synchronized block. The Lock API provides the tryLock() method. The thread acquires lock only if it’s available and not held by any other thread. This reduces the blocking time of thread waiting for the lock

This reduces the blocking time of thread waiting for the lock A thread that is in the “waiting” state to acquire access to a synchronized block, can’t be interrupted. The Lock API provides a method lockInterruptibly() which can be used to interrupt the thread when it’s waiting for the lock

Lock Reentrance

All implicit monitors implement reentrant characteristics. Reentrant means that locks are bound to the current thread. A thread can safely acquire the same lock multiple times without running into deadlocks (e.g. a synchronized method calls another synchronized method on the same object).

When the thread first enters into the lock, a hold count is set to one. Before unlocking the thread can re-enter into lock again and every time hold count is incremented by one. For every unlocks request, the hold count is decremented by one and when the hold count is 0, the resource is unlocked.

Lock Fairness

Java’s synchronized blocks make no guarantees about the sequence in which threads trying to enter them are granted access. Therefore, if many threads are constantly competing for access to the same synchronized block, there is a risk that one or more of the threads are never granted access — that access is always granted to other threads. This is called starvation. To avoid this a Lock should be fair.

Reentrant Locks also offer a fairness parameter, by which the lock would abide by the order of the lock request i.e. after a thread unlocks the resource, the lock would go to the thread which has been waiting for the longest time. This fairness mode is set up by passing true to the constructor of the lock.

Lock Methods

Let’s take a look at the methods in the Lock interface:

lock() — call to the lock() method increments the hold count by 1 and gives the lock to the thread if the shared resource is initially free.

call to the method increments the hold count by 1 and gives the lock to the thread if the shared resource is initially free. unlock() — call to the unlock() method decrements the hold count by 1. When this count reaches zero, the resource is released.

call to the method decrements the hold count by 1. When this count reaches zero, the resource is released. tryLock() — if the resource is not held by any other thread, then call to tryLock() returns true and the hold count is incremented by one. If the resource is not free then the method returns false and the thread is not blocked but it exists.

if the resource is not held by any other thread, then call to returns true and the hold count is incremented by one. If the resource is not free then the method returns false and the thread is not blocked but it exists. tryLock(long timeout, TimeUnit unit) — as per the method, the thread waits for a certain time period as defined by arguments of the method to acquire the lock on the resource before exiting.

as per the method, the thread waits for a certain time period as defined by arguments of the method to acquire the lock on the resource before exiting. lockInterruptibly() — this method acquires the lock if the resource is free while allowing for the thread to be interrupted by some other thread while acquiring the resource. It means that if the current thread is waiting for a lock but some other thread requests the lock, then the current thread will be interrupted and return immediately without acquiring a lock.

Lock Implementations

Multiple lock implementations are available in the standard JDK:

ReentrantLock

ReentrantReadWriteLock

StampedLock

ReentrantLock

The class ReentrantLock is a mutual exclusion lock with the same basic behavior as the implicit monitors accessed via the synchronized keyword but with extended capabilities. As the name suggests this lock implements reentrant characteristics just as implicit monitors.

A lock is acquired via lock() and released via unlock() . It's important to wrap your code into a try/finally block to ensure unlocking in case of exceptions. This method is thread-safe just like the synchronized counterpart. If another thread has already acquired the lock subsequent calls to lock() pause the current thread until the lock has been unlocked. Only one thread can hold the lock at any given time.

ReentrantReadWriteLock

ReentrantReadWriteLock class implements the ReadWriteLock interface. The interface ReadWriteLock specifies another type of lock maintaining a pair of locks for read and write access.

The idea behind read-write locks is that it's usually safe to read mutable variables concurrently as long as nobody is writing to this variable. So the read-lock can be held simultaneously by multiple threads as long as no threads hold the write-lock. This can improve performance and throughput in the case reads are more frequent than writes.

Let’s see rules for acquiring the ReadLock or WriteLock by a thread:

Read Lock — if no thread acquired the write lock or requested for it then multiple threads can acquire the read lock

— if no thread acquired the write lock or requested for it then multiple threads can acquire the read lock Write Lock — if no threads are reading or writing then only one thread can acquire the write lock

The above example first acquires a write-lock in order to put a new value to the map after sleeping for one second. Before this task has finished two other tasks are being submitted trying to read the entry from the map and sleep for one second:

When you execute this code sample you’ll notice that both read tasks have to wait the whole second until the writing task has finished. After the write lock has been released both read tasks are executed in parallel and print the result simultaneously to the console. They don’t have to wait for each other to finish because read-locks can safely be acquired concurrently as long as no write-lock is held by another thread.

StampedLock

Java 8 ships with a new kind of lock called StampedLock which also supports read and write locks just like in the example above. In contrast to ReadWriteLock the locking methods of a StampedLock return a stamp represented by a long value. You can use these stamps to either release a lock or to check if the lock is still valid.

Another feature provided by StampedLock is optimistic locking. Most of the time read operations don’t need to wait for write operation completion and as a result of this, the full-fledged read lock isn’t required.

Instead, we can upgrade to read lock:

Working With Conditions

The Condition class provides the ability for a thread to wait for some condition to occur while executing the critical section.

This can occur when a thread acquires access to the critical section but doesn’t have the necessary condition to perform its operation. For example, a reader thread can get access to the lock of a shared queue, which still doesn’t have any data to consume.

Traditionally Java provides wait(), notify() and notifyAll() methods for thread intercommunication. Conditions have similar mechanisms, but in addition, we can specify multiple conditions:

Semaphores

In addition to locks, the Concurrency API also supports counting semaphores. Whereas locks usually grant exclusive access to variables or resources, a semaphore is capable of maintaining whole sets of permits. This is useful in different scenarios where you have to limit the amount of concurrent access to certain parts of your application.

Here’s an example of how to limit access to a long-running task simulated by sleep(5) :

The executor can potentially run 10 tasks concurrently but we use a semaphore of size 5, thus limiting concurrent access to 5. It’s important to use a try/finally block to properly release the semaphore even in case of exceptions.

Executing the above code results in the following output:

Semaphore acquired

Semaphore acquired

Semaphore acquired

Semaphore acquired

Semaphore acquired

Could not acquire semaphore

Could not acquire semaphore

Could not acquire semaphore

Could not acquire semaphore

Could not acquire semaphore

The semaphores permit access to the actual long-running operation simulated by sleep(5) up to a maximum of 5. Every subsequent call to tryAcquire() elapses the maximum wait timeout of one second, resulting in the appropriate console output that no semaphore could be acquired.

Conclusion

In this article, we have seen different implementations of the Lock interface and learned how to use them in multithreaded applications.

In the next article, we will check the Executors. Stay tuned.