Generic Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we'll learn the difference between the volatile keyword and atomic classes and what problems they solve. First, it's necessary to know how Java handles the communication between threads and what unexpected issues can arise.

Thread safety is a crucial topic that provides an insight into the internal work of multithreaded applications. We'll also discuss race conditions, but we won't go too deep into this subject.

2. Concurrency Problem

Let's take a simple example to see the difference between atomic classes and the volatile keyword. Imagine that we're trying to create a counter that will be working in a multithreaded environment.

In theory, any application thread can increment this counter's value. Let's start implementing it with a naive approach and will check what problems will arise:

public class UnsafeCounter {
    
    private int counter;
    
    int getValue() {
        return counter;
    }
    
    void increment() {
        counter++;
    }
}

This is a perfectly working counter, but, unfortunately, only for a single-threaded application. This approach will suffer from visibility and synchronization problems in a multithreaded environment. In big applications, it might create difficulty to track bugs and even corrupt users' data.

3. Visibility Problem

A visibility problem is one of the issues when working in a multithreaded application. The visibility problem is tightly connected to the Java memory model.

In multithreaded applications, each thread has its cached version of shared resources and updates the values in or from the main memory based on events or a schedule.

The thread cache and main memory values might differ. Therefore, even if one thread updates the values in the main memory, these changes are not instantly visible to other threads. This is called a visibility problem.

The volatile keyword helps us to resolve this issue by bypassing caching in a local thread. Thus, volatile variables are visible to all the threads, and all these threads will see the same value. Hence, when one thread updates the value, all the threads will see the new value. We can think about it as a low-level observer pattern and can rewrite the previous implementation:

public class UnsafeVolatileCounter {
    
    private volatile int counter;
    
    public int getValue() {
        return counter;
    }
    
    public void increment() {
        counter++;
    }
}

The example above improves the counter and solves the problem with visibility. However, we still have a synchronization problem, and our counter won't work correctly in a multithreaded environment.

4. Synchronization Problem

Although volatile keyword helps us with visibility, we still have another problem. In our increment example, we perform two operations with the variable count. First, we read this variable and then assign a new value to it. This means that the increment operation isn't atomic.

What we're facing here is a race condition. Each thread should read the value first, increment it, and then write it back. The problem will happen when several threads start working with the value, and read it before another one writes it.

This way, one thread may override the result written by another thread. The synchronized keyword can resolve this problem. However, this approach might create a bottleneck, and it's not the most elegant solution to this problem.

5. Atomic Values

Atomic values provide a better and more intuitive way to handle this issue. Their interface allows us to interact with and update values without a synchronization problem.

Internally, atomic classes ensure that, in this case, the increment will be an atomic operation. Thus, we can use it to create a thread-safe implementation:

public class SafeAtomicCounter {
    private final AtomicInteger counter = new AtomicInteger(0);
    
    public int getValue() {
        return counter.get();
    }
    
    public void increment() {
        counter.incrementAndGet();
    }
}

Our final implementation is thread-safe and can be used in a multithreaded application. It doesn't differ significantly from our first example, and only by using atomic classes could we resolve visibility and synchronization problems in the multithreaded code.

6. Conclusion

In this article, we learned that we should be very cautious when we're working in a multithreading environment. The bugs and issues can be tough to track down and probably won't appear while debugging. That's why it's essential to know how Java handles these situations.

The volatile keyword can help with visibility problems and resolve the issue with intrinsically atomic operations. Setting a flag is one of the examples where the volatile keyword might be helpful.

Atomic variables help with handling non-atomic operations like increment-decrement, or any operations, which need to read the value before assigning a new one. Atomic values are a simple and convenient way to resolve synchronization issues in our code.

As always, the source code for the examples is available over on GitHub.

Generic bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
Generic footer banner
guest
0 Comments
Inline Feedbacks
View all comments