DEV Community

Pranav Jandu
Pranav Jandu

Posted on

Critical Sections and Race Conditions: Ensuring Thread Safety in Concurrent Programs

A well-designed program is a set of instructions that can be executed concurrently by multiple threads. However, when these threads attempt to execute the same section of code simultaneously, caution must be exercised to ensure data integrity and prevent race conditions.

Understanding Critical Sections

A critical section is a portion of the code that may be accessed concurrently by more than one thread of the application and exposes shared data or resources. These shared resources could include variables, files, or any other data structure that multiple threads need to access or modify.

Unraveling the Race Condition

A race condition occurs when multiple threads access shared resources or program variables without proper thread synchronization. In such a scenario, the outcome of the program becomes dependent on the relative execution order of the threads, leading to inconsistent and unpredictable results.

Imagine two threads attempting to modify the same shared variable simultaneously. Depending on the execution order, one thread might overwrite the changes made by the other, leading to a data inconsistency.

Let us see an example.

public class RaceConditionExample {
    private static int counter = 0;
    private static final int NUM_THREADS = 10;
    private static final int NUM_ITERATIONS = 10000;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[NUM_THREADS];

        for (int i = 0; i < NUM_THREADS; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < NUM_ITERATIONS; j++) {
                    // Increment the counter in a non-atomic way
                    int currentValue = counter;
                    counter = currentValue + 1;
                }
            });
        }

        for (int i = 0; i < NUM_THREADS; i++) {
            threads[i].start();
        }

        for (int i = 0; i < NUM_THREADS; i++) {
            threads[i].join();
        }

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

Enter fullscreen mode Exit fullscreen mode

In this example, we have a shared static variable counter, and we create multiple threads to increment it. The threads each perform a loop where they read the current value of the counter, increment it by 1, and then store it back. However, since these operations are not atomic, a race condition can occur.

When multiple threads access and modify the counter concurrently, the program's output becomes unpredictable due to the race condition. The final value of counter will not necessarily be NUM_THREADS * NUM_ITERATIONS, as you might expect. Instead, it could be lower due to some threads overwriting each other's changes.

To protect critical sections, developers often implement synchronization mechanisms such as locks, semaphores, or mutexes. These mechanisms ensure that only one thread can access the critical section at a time, preventing data inconsistencies caused by concurrent access.

Top comments (0)