Authors Top

If you have a few years of experience in the Java ecosystem, and you’d like to share that with the community, have a look at our Contribution Guidelines.

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 look at the differences between the methods set() and lazySet() of Java atomic classes like AtomicInteger and AtomicReference.

2. Atomic Variables – A Quick Recap

Atomic variables in Java allow us to easily perform thread-safe operations on class references or fields without having to add concurrency primitives like monitors or mutexes.

They've defined under the java.util.concurrent.atomic package, and although their APIs are different depending on the atomic type, most of them support the set() and lazySet() methods.

To make things simple, we'll use AtomicReference and AtomicInteger throughout this article, but the same principles apply to other atomic types.

3. The set() Method

The set() method is equivalent to writing to a volatile field.

After calling set(), when we access the field using the get() method from a different thread, the change is immediately visible. This means that the value was flushed from the CPU cache to a memory layer common to all CPU cores.

To showcase the above functionality, let's create a minimal producer-consumer console app:

public class Application {

    AtomicInteger atomic = new AtomicInteger(0);

    public static void main(String[] args) {
        Application app = new Application();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                app.atomic.set(i);
                System.out.println("Set: " + i);
                Thread.sleep(100);
            }
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (app.atomic) {
                    int counter = app.atomic.get();
                    System.out.println("Get: " + counter);
                }
                Thread.sleep(100);
            }
        }).start();
    }
}

In the console, we should see a series of “Set” and “Get” messages:

Set: 3
Set: 4
Get: 4
Get: 5

What indicates cache coherence is the fact that values in the “Get” statements are always equal to or greater than those in the “Set” statements above them.

This behavior, although very useful, comes with a performance hit. It'd be great if we could avoid it in cases where we don't need the cache coherence.

4. The lazySet() Method

The lazySet() method is the same as the set() method but without the cache flushing.

In other words, our changes are only eventually visible to other threads. This means that calling get() on the updated AtomicReference from a different thread might give us the old value.

To see this in action, let's change the first thread's Runnable in our previous console app:

for (int i = 0; i < 10; i++) {
    app.atomic.lazySet(i);
    System.out.println("Set: " + i);
    Thread.sleep(100);
}

The new “Set” and “Get” messages might not always be incrementing:

Set: 4
Set: 5
Get: 4
Get: 5

Due to the nature of threads, we might need a few re-runs of the app in order to trigger this behavior. The fact that the consumer thread retrieves the value 4 first even though the producer thread has set the AtomicInteger to 5 means that the system is eventually consistent when lazySet() is used.

In more technical terms, we say that lazySet() methods don't act as happens-before edges in the code, in contrast to their set() counterparts.

5. When to Use lazySet()

It's not immediately clear when we should use lazySet() since its differences with set() are subtle. We need to carefully analyze the problem, not only to make sure that we'll get a performance boost but also to ensure correctness in a multi-threaded environment.

One way we can use it is to replace an object reference with null once we no longer need it. This way, we indicate that the object is eligible for garbage collection without incurring any performance penalties. We assume that the other threads can work with the deprecated value until they see the AtomicReference is null.

Generally, though, we should use lazySet() when we want to make a change to an atomic variable, and we know that the change doesn't need to become immediately visible to other threads.

6. Conclusion

In this article, we had a look at the differences between set() and lazySet() methods of atomic classes. We also learned when to use which method.

As always, the source code for the examples can be found 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
Comments are closed on this article!