Concurrency in Java
The need for synchronization
The need for synchronization appears when there are multiple threads trying to access the same resource which is shared and limited. For example, the classic example of multiple threads trying to access a printer. A printer can only serve one thread at a time.
Let’s take a deep dive into an example. Consider you have an e-commerce website where you need to manage inventory of the products.
Consider this simple class that manages inventory below
This is not an ideal class but well curated for the concept we are discussing.
Consider we have a thread, which might be used when customers are placing their orders. This thread takes in the quantity added to cart by the customer, and decreases the quantity in the inventory. We have a method decreaseQty() added for that purpose. The method first checks if the quantity is available or else throws an exception.
Let’s say that writing the inventory to a database takes 5 seconds as we want to try imitating real life scenarios. In real life writing to a database doesn’t take 5 seconds but threads sometimes do take some time to completely execute the code when they are in a waiting state (for example making network calls and getting no response or there is a delay in response in seconds)
Below is the code of the thread that will use the above inventory class
In the example above, we have assumed that we need 3 quantities of a product and hence we are setting the quantity = current quantity - 3
Let’s say two customers try to place an order at the same time, which is easily possible, and two instances of the above threads start executing simultaneously. This is where the trouble begins.
To imitate the scenario, consider the below class
In this class we are creating two instances of the WriterThread which will try to decrease the quantity of the product by 3.
You can see that in the inventory management class we have initialized the quantity with 5 to imply that we only have a total of 5 quantity available.
If we run the above class, we get the following output:
This is wrong! At least any one thread should not be able to proceed forward. We had a total quantity of 5 and both the threads were able to move ahead (which means the total quantity served was 6)
What happened was writerThread1 started → fetched the quantity → received it as 5 →proceeds to update the quantity → taking 5 seconds to update it.
Somewhere around this time (while writerThread1 is updating the quantity within those 5 seconds) writerThread2 starts parallely, but as writerThread1 has not completed it’s execution, the quantity in inventory is not updated yet. So when writerThread 2 tries to decrease the quantity, it again receives it as 5 and goes on to complete it’s execution.
Now writerThread1 updates the quantity to 5–3 = 2
And writerThread2 updates again to 2–3 = -1
The solution here is that only one thread should have been able to update the inventory at a given time. As inventory is a shared resource so whenever a thread is using a shared resource, we say that the thread has entered a critical section.
Allowing only one thread to run a critical section can be done using synchronization.
In our example, no two threads should be able to call decreaseQty() method simultaneously, and that solves our problem.
In Java we can simply do so by adding synchronized keyword in the method signature
Below is the updated code for inventory class
The only difference is the added synchronized keyword, and now only one thread will be able to call decreaseQty() method at a given time.
Now if we run the code again. We get the below output.
Here writerThread2 will wait till writerThread1 has completed it’s execution of decreaseQty() and when writerThread2 will start execution it will get the updated quantity which cannot be satisfied.
Synchronization blocks the entire object
When there are multiple methods in a class which are synchronized, any other thread cannot execute other synchronized method of the same object even though it can be allowed to be executed parallelly.
To overcome this we can use locks. We can have separate lock for each method.
For example, if we had another method in our inventory class as below
And let’s say thread A is executing decreaseQty(), then thread B cannot access logger() even if it perfectly fine to do so (just because both methods are synchronized)
As long as threads are sharing the same instance of inventory class, any thread can only access only one synchronized method at a time.
This is totally uncessary and is a drawback of synchronization. For example, if we have a thread that just wants to log something using the logger() method
And in our Main class
If we run above we get the following output
The three threads are called parallelly. But when logging starts, inventory update is not started unless the logging is completed. That’s because we have both the method synchronised, and only one thread can access one synchronised method of an object at a given time, even when both the methods are totally different in what they do.
This is a drawback of synchronization, and it can be overcome using locks.
Locks
Locks provide us more control on what the threads can execute. They also provide time based locking where a thread can wait for a certain amount of time to acquire a lock and if it fails to do so can take some action.
In our example, we can make use of locks to make a thread acquire a lock for one single method, let’s say decreaseQty(). Now if other thread wants to execute decreaseQty(), it is not possible to do so if a thread already has acquired a lock. But if a thread wants to execute logger() method, if no other method has a lock on it, it can safely do so.
This allows two threads to execute decreaseQty() and logger() simultaneously, and also not allow two threads to run the same method, either decreaseQty() or logger() at the same time.
Thus with the help of locks we can allow two methods of the same class to be run parallelly.
Java provides various implementations of locks in java.util.concurrent.locks
To demonstrate the use of locks, I am using ReentrantLock implementation. This implementation allows a thread to lock a resource when entering critical section
We need to change our method code to implement locks as below
Here I am creating two lock objects, one for each method decreaseQty() and logger()
Inside each method, a thread will acquire a lock and will unlock it once it completes execution.
So in our example, there are two threads writerThread1 and writerThread2 that will try to simultaneously run the decreaseQty() method, but only one will be able to acquire a lock at a given time.
We have another thread LoggerThread which will try to run logger(), but as there is no other thread that acquires a lock on it, it should be able to run simultaneously irrespective if any other thread runs any other method.
This allows logger() and decreaseQty() to be executed simultaneously by multiple threads. Note that still only one thread can execute decreaseQty() at a given time, because that’s what the lock is for.
Let’s see the output if we run our Main class
We can see that logging started and at the same time inventory got updated before logging got completed.
There are other lock implementations in the same package. You can read about them here: https://www.baeldung.com/java-concurrent-locks