The above image shows the interfaces that are included in the java.util.concurrent.locks package.
In Part 1 of this series, we explored the contents inside the java.util.concurrent package. If you haven’t already, I would recommend reading that part first.
Interfaces and Classes in this package define a framework for locking and waiting for conditions that are different from the built-in synchronization and thus providing greater flexibility.
Let’s take a look at the Lock Interface and its implementation classes.
Lock implementation provides the flexibility to attain and release locks at different scopes. For example, the synchronized method or blocks apply the lock over the complete object and the release and acquisition of a lock is performed in a block-structured way.
Start 1st thread Start 2nd thread In sync method Thread 1 Value returned Thread 1 In other method Thread 2
In the above example, only after the entire block in the getValue() method is executed that the lock is released. After the lock is released by Thread 1, Thread 2 acquires it and executes the getSomething() method.
(Note that the above code is just to emphasize the block-structure execution, we will never need to synchronize two get methods unless there is a put in the picture)
Let’s rewrite the same code by using a Lock implementation.
In the example with Lock, we are acquiring a lock only when it's needed. This lock can be acquired and released in different scopes. When Thread 1 acquires the lock to getValue() method, Thread 2 can acquire the lock to getSomething() method.
The output of the second program(one with Lock) is:
Start 1st thread In sync method Thread 1 Start 2nd thread In other method Thread 2 in unlock Thread 1 Value returned Thread 1
Another functionality that the Lock provides is to check if a lock is available before entering the wait state by using tryLock() or one with timeout using tryLock(long, TimeUnit).
Next, let’s take a look at the ReadWriteLock Interface.
This interface provides methods two acquire read and write lock. For methods that only need read access to a shared resource a read lock can be acquired and for methods that need write access can create a write lock.
In the below example, we will create two Threadsthat will call testLock() method. This method is partially locked by read lock and partially locked by write lock. We can see from the output that both Threads can simultaneously execute the code that's read locked but only one Thread enters the write lock at a time.
The output of the above program
Start 1st thread Start 2nd thread In read lock Thread 2 In read lock Thread 1 in read unlock Thread 2 in read unlock Thread 1 In write lock Thread 1 In write unlock Thread 1 Method Ends... Thread 1 In write lock Thread 2 In write unlock Thread 2 Method Ends... Thread 2
While working on Lock implementations, we must be careful enough to unlock the acquired locks in the finally block so that even in cases of errors we will release the lock.
In synchronized blocks, we do not have to worry about releasing locks.
Now, let’s take a look at the Condition Interface.
A Condition instance is bound to a Lock, that is, to instantiate a Condition object we would need to get that instance from a Lock object.
Lock l = new ReentrantLock(); Condition condition = l.newCondition();
Condition objects act upon a single thread to suspend it using await() method.
and resumes the activity once notified by another thread using signal() method.
Class ArrayBlockingQueue makes use of Condition to block the thread when it's empty or full.
Next, Let’s explore the classes and interfaces contained by the java.util.concurreny.atomic package.
Remember, at the beginning of this article we made an instance variable Thread-safe by synchronizing the methods. We know that by synchronizing the methods we are applying lock over an instance and blocking all threads except one. If we want to avoid applying a lock on threads we can use the classes provided under the atomic package.
An overhead however is that we need to check the existing value stored in the memory to the one that the Thread operating on the variable has using compareAndSet() method. If both match, the new value is updated else it's not. So we are expected to take care of the scenarios where the write operation did not succeed.
In this two-part series, we have explored the most commonly used Classes and Interfaces in the java.util.concurrent package and its sub-packages.
In most of the real use cases related to web applications, one might not use the implementation of Lock and instead use the synchronized options but it’s always good to know the available options.