DEV Community

Code Craft Club
Code Craft Club

Posted on • Originally published at Medium

Mutex & Race Conditions in Java Multi-Threading made so simple with real-life analogies

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

  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

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

  • 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

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

Bounded

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

Busy waiting

MUTEX (Mutual Exclusion)

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

Mutex

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

The Adder class:

Adder

The Subtractor class:

Subtractor

The Main class:

Main

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

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!

Top comments (0)