← Back to Multithreading

Understanding Race Conditions

Understand why multiple threads modifying the same data can sometimes produce incorrect results.

1. Let's Revisit Our Counter Example

In our previous class, we learned that multiple threads can access the same variable or object.

Now imagine that we have a counter whose initial value is zero.

Two threads are working with this same counter. Each thread wants to increase its value by one.

What should the final value be?

The answer should be 2. One thread increases it to 1, and the other increases it to 2.

But there is a problem. Let's understand it.

2. Is counter++ Just One Operation?

When we write:

counter++;

It looks like one simple operation.

But internally, increasing a value involves three basic steps:

  1. Read: Read the current value of the counter.
  2. Increase: Add one to that value.
  3. Write: Store the updated value back.

Let's say the counter currently contains 0.

A thread reads 0, increases it to 1, and writes 1 back to the counter.

The important thing is that these steps are not guaranteed to happen as one indivisible operation.

3. How Can the Result Become Incorrect?

Let's see what happens when two threads try to increase the same counter at almost the same time.

The counter starts at 0.

Imagine the following sequence:

  1. Thread 1 reads the counter value: 0.
  2. Thread 2 also reads the counter value: 0.
  3. Thread 1 increases its value to 1.
  4. Thread 2 also increases its value to 1.
  5. Thread 1 writes 1 to the counter.
  6. Thread 2 writes 1 to the counter.

What is the final counter value?

It is 1, not 2!

Both threads performed an increment, but one update was effectively lost.

This is commonly called a lost update.

4. Visualising the Problem

Here is the same sequence in a simple table.

Step Thread 1 Thread 2 Counter
Initial value - - 0
1 Reads 0 - 0
2 - Reads 0 0
3 Calculates 1 - 0
4 - Calculates 1 0
5 Writes 1 - 1
6 - Writes 1 1

Both threads started with the same value. Both calculated the same updated value. One update overwrote the other.

5. What Is a Race Condition?

We have just seen two threads competing to modify the same counter.

The final result depended on the order in which their operations happened.

This kind of problem is called a race condition.

A race condition occurs when the result of a program depends on the timing or order of operations performed by multiple threads accessing shared data.

In simple words:

When multiple threads compete to access or modify shared data, the final result can become unpredictable.

6. A Java Example of a Race Condition

Let's write a small program to demonstrate the problem.

class Counter {

    int value = 0;

    void increment() {
        value++;
    }
}

public class RaceConditionExample {

    public static void main(String[] args)
            throws InterruptedException {

        Counter counter = new Counter();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                counter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                counter.increment();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final counter value: "
                + counter.value);
    }
}

Let's understand the program.

  1. We create one Counter object.
  2. We create two threads that share this object.
  3. Each thread increments the counter 100,000 times.
  4. We use join() so the main thread waits for both threads to finish.
  5. Finally, we print the counter value.

What should the final value be?

We expect:

Final counter value: 200000

But because both threads are modifying the same counter without protection, the actual result may be lower.

The exact result can vary between executions. A race condition is possible, but the program may sometimes produce the expected result too.

Also notice that we used join() here. It ensures that we print the result only after both threads finish. However, it does not protect the counter from simultaneous updates.

7. How Do We Prevent Race Conditions?

The main idea is to coordinate access to shared data.

When one thread is modifying a shared value, we may need to prevent another thread from modifying that same value at the same time.

Java provides mechanisms to help us do this.

One important mechanism is called synchronization.

We will learn about synchronization in our next class. For now, understand the problem clearly:

Multiple threads modifying shared data without proper coordination can lead to race conditions.

8. What Did We Learn?

  • The counter++ operation involves reading, increasing, and writing a value.
  • These steps are not guaranteed to happen as one indivisible operation.
  • Two threads can read the same old value and overwrite each other's updates.
  • This can result in a lost update.
  • A race condition occurs when the result depends on the timing or order of operations.
  • join() makes the main thread wait, but does not make shared updates safe.
  • Synchronization is one way Java helps coordinate access to shared data.

Remember: A race condition can occur when multiple threads access and modify shared data without proper coordination.

Test Your Understanding

Let's check what you have learned in this class.

1. What is a race condition?

2. What are the basic steps involved in counter++?

3. Two threads read a counter value of 0 and both calculate 1. What can happen?

4. Does join() prevent race conditions?

5. Which Java mechanism will we learn next to coordinate access to shared data?

Class Summary

Let's quickly revise what we learned in this class.

  • A race condition can occur when multiple threads access and modify shared data.
  • counter++ involves reading, increasing, and writing a value.
  • Two threads can read the same old value and overwrite each other's updates.
  • This can result in a lost update and an incorrect final value.
  • join() does not protect shared data from simultaneous updates.
  • Synchronization is one way to coordinate access to shared data.

We now understand why shared data needs protection when multiple threads modify it.

What's Next?

Class 13: Synchronization with synchronized Methods

We have seen how two threads can accidentally overwrite each other's updates.

But how can we prevent this from happening?

In the next class, we will learn about Java's synchronized keyword and how synchronized methods help protect shared data.

We will use our counter example again and see how synchronization helps us get the correct result.

See you in the next class!

0% Completed

You have completed 12 out of 46 classes.