Photo by Yagnik Sankhedawala on Unsplash
Previous articles :
In our previous article, I have discussed race conditions. A race condition happens in a critical section. In this article, I will discuss this critical section and why it happens when it happens.
Let’s begin.
We got some ideas from the previous articles a data can be shared among multiple threads. Multiple threads can read a shared data, and it has no consequences. When one of these threads tries to change/write to the shared data, the problem comes to the picture. An example would be the best approach to explain this-
package com.bazlur;
public class Day022 {
public static void main(String[] args) throws InterruptedException {
var account = new BankAccount(0);
Runnable runnable = () -> {
for (int i = 0; i < 1000; i++) {
account.deposit(100);
account.withdraw(100);
}
};
var t1 = new Thread(runnable);
var t2 = new Thread(runnable);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Available balance: " + account.getBalance());
}
}
final class BankAccount {
private long initialAmount;
BankAccount(long initialAmount) {
this.initialAmount = initialAmount;
}
public void withdraw(long amount) {
deposit(-amount);
}
public void deposit(long amount) {
initialAmount = initialAmount + amount;
}
public long getBalance() {
return initialAmount;
}
}
In the above code, we have simulated a bank account. We have created two threads, and both of their deposit and withdraw 100 thousand times. According to our calculation, the initial balance was 0, which means it should zero in the end. I run the above code; that’s not what we would see. Each time it will print a different result. That’s means something wrong here.
Is it a visibility problem? What if both threads run in two different processors and update their value in the cache? If we add a volatile keyboard and would that solve the issue? We can try adding a volatile keyword, but it won’t solve the problem because this isn’t a visibility issue, something else. Let’s discuss that.
We know in a multi-threaded environment, the execution order of code isn’t predictable. While the first thread is depositing the money, the second thread might well withdraw the amount. While both threads contest to write the value to the same variable, one might get discarded, and the other can win. If one of these operations are missed, then we will have incorrect data. That’s a problem.
In fact, in our code, that’s precisely what is happening.
How do we resolve it?
Well, we have to make the writing operation atomic. Changing the variable has to be done in a way so that only one thread can do it at a time. That way, we can prevent this inconsistency. This is called the critical section. We have to guard this critical section.
We have to use a mutual exclusion so that no other thread can go into that section if a thread reaches into the critical section and make the changes. Once a get away from the critical section, only then will the other get a chance.
This mutual execution is done by a keyword called synchronized
. This keyword instructs java to create a lock. When a thread reaches the critical section, and it tries to acquire the lock. If it can acquire the lock, it does the operation, and once done, it releases the lock. Meanwhile, the other threads try to acquire the lock and wait until it is released.
When only one thread is allowed to perform an operation, then the operation is called atomic. There are multiple ways to achieve this atomicity. The easiest one is, declaring the method synchronized. Example –
class BankAccount {
private long initialAmount;
BankAccount(long initialAmount) {
this.initialAmount = initialAmount;
}
public synchronized void withdraw(long amount) {
deposit(-amount);
}
public synchronized void deposit(long amount) {
initialAmount = initialAmount + amount;
}
public synchronized long getBalance() {
return initialAmount;
}
}
When we use mutual exclusion with synchronized keyword or lock, it also ensures visibility. So we don’t need to use volatile here. We can even create our own lock object. For example-
final class BankAccount {
private final Object lock = new Object();
private volatile long initialAmount;
BankAccount(long initialAmount) {
this.initialAmount = initialAmount;
}
public void withdraw(long amount) {
synchronized (lock) {
deposit(-amount);
}
}
public void deposit(long amount) {
synchronized (lock) {
initialAmount = initialAmount + amount;
}
}
public long getBalance() {
synchronized (lock){
return initialAmount;
}
}
}
The above code does the same thing; only it doesn’t have the synchronized keyword in the method signature. When we use synchronized keyword in the method signature, it is effectively synchronizing to the object itself. With the synchronized block, we can have multiple lock objects inside a class. For example, a class may have multiple critical sections, and they may not be related to each other. We can certainly allow multiple threads to use this class that deal with different critical sections. In such a scenario, multiple locks would make sense.
However, in modern java, we have great APIs with many other functionalities and flexibility in how we lock. These modern concurrency APIs don’t use the synchronized
keyword at all. The lock can be conditional, and based on the situation, we can lock and unlock. But that’s something we will discuss some other day. Let’s just implement the BankAccount
class with the Lock API.
class BankAccount {
private long initialAmount;
private final Lock lock = new ReentrantLock();
BankAccount(long initialAmount) {
this.initialAmount = initialAmount;
}
public void withdraw(long amount) {
lock.lock();
try {
deposit(-amount);
} finally {
lock.unlock();
}
}
public void deposit(long amount) {
lock.lock();
try {
initialAmount = initialAmount + amount;
} finally {
lock.unlock();
}
}
public long getBalance() {
lock.lock();
try {
return initialAmount;
} finally {
lock.unlock();
}
}
}
Or we can use the LockHelper
from the previous article.
class BankAccount {
private final Lock lock = new ReentrantLock();
private volatile long initialAmount;
BankAccount(long initialAmount) {
this.initialAmount = initialAmount;
}
public void withdraw(long amount) {
LockHelper.withLock(lock, () -> deposit(-amount));
}
public void deposit(long amount) {
LockHelper.withLock(lock, () -> initialAmount = initialAmount + amount);
}
public long getBalance() {
return LockHelper.withLock(lock, () -> initialAmount);
}
}
That’s for today.
Cheers!!
Top comments (0)