Mutex & Race Conditions in Java Multi-Threading made so simple with real-life analogies
Tags: java, multithreading, programming, softwareengineering
Published: 2023-09-15 18:58:50

Multithreading is a powerful concept in Java, allowing you to run multiple threads of execution concurrently. However, with great power comes great responsibility, and when multiple threads access shared resources, it can lead to synchronization issues. Let's explore the need for synchronization, the issues that arise without it, and the solutions available in Java.

## The Need for Synchronization

Imagine you have a bank account with a balance of $100, and two threads are running concurrently: one is trying to withdraw money, and the other is attempting to deposit money.


![Bank](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qosx0zf4s8isny3zqseb.jpeg)

1. Thread A (Withdraw) reads the current balance, which is $100.
2. Thread B (Deposit) also reads the same current balance, which is still $100.
3. Thread A calculates the new balance after withdrawing $50, resulting in $50.
4. Thread B calculates the new balance after depositing $30, resulting in $130.
5. Thread A updates the balance to $50.
6. Thread B updates the balance to $130.

In this scenario, both threads are not synchronized, and they read and modify the balance concurrently. As a result, the final balance is $130, even though the expected behavior should be a balance of $80 ($100 - $50 + $30).

In multithreading, synchronization is essential to ensure that threads can safely access shared resources (like variables, data structures, or files) without causing conflicts or inconsistencies.

## ****Three Reasons for Synchronization Issues****

### 1. Critical Section

**_A critical section in multithreading refers to a section of code or a block of code where multiple threads may access shared resources or data concurrently_**. To ensure correct and consistent behavior, only one thread should be allowed to execute the critical section at a time while other threads wait their turn. This synchronization is achieved using techniques like locks, mutexes (mutual exclusion), or semaphores.


![Critical Section](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nc75kfj4b0gbwn7noohr.png)

### 2. Race Conditions

A race condition can be described as a situation where **_more than one thread attempts to enter a critical section (a section of code where shared resources are accessed) at the same time_**, leading to unpredictable and incorrect behavior.

### 3. **Preemptiveness**

Preemptive multitasking as the name suggests “Pre-Emptying”, allows the operating system to interrupt a thread and give control to another.

> 
Synchronization problem occurs only when all the above three conditions are met. 
To resolve synchronization issues, we need to address any one condition amongst Critical Section, Race Conditions and Preemptiveness.
>

## Ideal Properties of a Synchronization Solution

A good synchronization solution should possess the following properties:

- **Mutual Exclusion**: Only one thread exclusively should have access to the critical section at a time, preventing interference. In other terms, there must not be any race conditions.

![Mutual Exclusion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u3z5fiyiik0ad8gt3amz.png)

- **Progress of the Program**: The synchronization should not lead to deadlock, where threads are blocked indefinitely and bring the entire program to a halt. It should allow other threads (at least one thread) to execute when a thread is waiting.

![Progress](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xf48vhj00mge131s0kh7.png) 

- **Bounded Waiting**: A thread should not have to wait indefinitely to enter a critical section. There should be a limit on waiting time.

![Bounded](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/720vcs50iwcp5h6f2veq.png)

- **No Busy Waiting**: Busy waiting consumes CPU cycles needlessly. An efficient synchronization solution should not force threads to continuously check for access.

![Busy waiting](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/umjmfdk66lqopriy7s10.png)

## MUTEX (Mutual Exclusion)

A mutex, short for "mutual exclusion," is a synchronization mechanism that serves as a lock to ensure mutual exclusion.

![Mutex](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bdcxwx5ovfn6w9ao41k5.png)

### **Properties of a Mutex Lock in Java**

1. **Mutual Exclusion**: Only one thread can acquire the mutex lock at a time, preventing concurrent access to shared resources.
2. **Ownership**: The thread that acquires the mutex lock is the only one that can release it, ensuring proper resource management.
3. **Blocking**: If a thread attempts to acquire a locked mutex, it will be blocked until the mutex is released by the owning thread.
4. **Non-busy Waiting**: Threads waiting for the mutex are not actively spinning in a loop but are put in a waiting state until the mutex is available.

## Synchronization in Adder & Subtractor

Let’s understand the need of synchronization with an example of Adder and Subtractor.

In this code, we will have two classes: **`Adder`** and **`Subtractor`**. These classes represent two concurrent operations that manipulate a shared resource.

- **Adder:** The **`Adder`** class is responsible for adding numbers from 1 to 100 to a shared value.
- **Subtractor:** The **`Subtractor`** class is responsible for subtracting numbers from 1 to 100 from the same shared value.

Let's create the classes as follows:

#### The `SharedResource` class:

![Shared Resource](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgenk2ii0px7supcwdxv.png)

#### The `Adder` class:

![Adder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8hw621incbhlsma8vfk7.png)

#### The `Subtractor` class:

![Subtractor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qej1ptrumlctxfc76y6k.png)

#### The `Main` class:

![Main](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rozyb7vdflrh9hi64b3y.png)

In this code:

1. **`SharedResource`** is a class that contains the shared value.
2. **`Adder`** and **`Subtractor`** are two separate threads that add and subtract values from the shared resource, respectively.
3. The **`Main`** class creates instances of **`Adder`** and **`Subtractor`**, starts them concurrently, and then waits for them to finish using **`join()`**.

When you run this code, you'll likely observe that the final shared value is not always 0, which indicates a synchronization issue because the **`add`** and **`subtract`** operations are not synchronized.

### **Behavior of the code Without Locks:**

Without any synchronization mechanism (locks), both the **`Adder`** and **`Subtractor`** threads run concurrently and can access the shared resource simultaneously. This can lead to race conditions, where the order of execution and interleaving of operations between the threads are unpredictable. As a result, the final value of the shared resource can vary each time the program is run.

### **Implementation for Synchronization:**

In Java, the **`ReentrantLock`** is a common implementation of a Mutex.

To make the code synchronous and prevent race conditions, we use **`ReentrantLock`** to protect the critical sections of code, specifically the **`add`** and **`subtract`** methods in the **`SharedResource`** class. Here's how it's done:


![Shared Resource with lock](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xdtyg56q3hpxe21d9kry.png)

By acquiring and releasing the lock using **`lock.lock()`** and **`lock.unlock()`** respectively, we ensure that only one thread can execute the critical sections of the **`add`** and **`subtract`** methods at any given time, preventing concurrent interference and ensuring consistent and correct results.

With these modifications, the code becomes synchronized, and the final shared value will always be 0, as expected, regardless of how many times you run the program.

### **Conclusion**

The **`ReentrantLock`** was used, ensuring that only one thread could access the critical sections at a time. This synchronization mechanism resolved race conditions and ensured that the final shared value was always 0, as expected.

While locks are a common and effective way to achieve synchronization, there are other methods and techniques as well such as **Synchronized Methods, Synchronized Blocks and Semaphores.**

### Closing Thoughts:
Thank you for reading our blog. We appreciate your time and interest in our content. If you have any questions, feedback, or topics you'd like us to cover in our future articles, please feel free to leave a comment below. Your input is valuable, and it helps us create content that matters to you.

Stay connected with us for more insightful articles on various topics related to technology, programming, and much more.

Happy coding!