Java Concurrency - Atomic Variable

Atomic variables usage

Atomic Variables

Atomic variables in Java, found within the java.util.concurrent.atomic package, are useful for managing single shared variables across multiple threads, ensuring thread safety without explicit locks. They achieve this through atomic operations like compare-and-swap (CAS), incrementing or decrementing values, and setting new values.

Use Cases

Atomic variables are suitable for simple shared state management in concurrent applications, where multiple threads need to access and update a single variable. They provide a lightweight and efficient alternative to locks and synchronized blocks, avoiding the overhead of context switching and potential deadlocks.

When to use atomic variables:

Simple counters and sequences: When multiple threads need to increment or decrement a shared counter, atomic variables like AtomicInteger or AtomicLong provide thread-safe operations.
Flag variables: For simple boolean flags shared between threads, AtomicBoolean ensures that updates are visible and atomic.
Optimistic locking: When implementing optimistic locking mechanisms, AtomicReference can be used to update a reference to an object only if it hasn’t been changed by another thread in the meantime.
High contention scenarios: Atomic operations can be more efficient than locks when contention is high because they avoid the overhead of context switching.
Non-blocking algorithms: Atomic variables are fundamental in building non-blocking algorithms, where threads don’t have to wait for locks, preventing deadlocks.

When not to use atomic variables:

Multiple variables: When you need to perform atomic operations on multiple variables as a single unit, atomic variables are insufficient. Use locks or synchronized blocks instead.
Complex operations: If the operations on shared data are more complex than simple updates, atomic variables might not be suitable. Consider using locks or other synchronization mechanisms.
Low contention scenarios: If contention is low, the overhead of atomic operations might be higher than using simple synchronization.
Performance is not critical: If performance is not a major concern, using synchronized blocks or methods might be simpler and easier to understand.
When atomicity is already guaranteed: Reads and writes of primitive variables (except long and double) are atomic in Java. Thus, for single reads and writes, using AtomicInteger or AtomicBoolean might be unnecessary.

Example

Here we have an unsafe counter class that is not thread-safe. Multiple threads increment the counter

1
2
3
4
5
6
7
8
9
10
11
public class UnsafeCounter {
private int count;

public void increment() {
count++;
}

public int getCount() {
return count;
}
}

To make it thread-safe, we can use an AtomicInteger from the java.util.concurrent.atomic package, which provides atomic operations for incrementing and getting the value.

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadSafeCounter {
private final AtomicInteger count = new AtomicInteger(0);

public void increment() {
count.incrementAndGet();
}

public int getCount() {
return count.get();
}
}

Test the thread-safe counter with multiple threads incrementing the counter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ThreadSafeCounter counter = new ThreadSafeCounter();

int numberOfThreads = 10;
int incrementsPerThread = 1000;
Thread[] threads = new Thread[numberOfThreads];

for (int i = 0; i < numberOfThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
counter.increment();
try {
Thread.sleep(10); // Adding sleep to simulate real-world conditions
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
for (Thread thread : threads) {
thread.start();
}

for (Thread thread : threads) {
thread.join();
}

System.out.println("counter value =" + counter.getCount()); // should be numberOfThreads * incrementsPerThread

Atomic Reference Example

AtomicReference can be used to implement optimistic locking, where a reference to an object is updated only if it hasn’t been changed by another thread. Here’s an example of using AtomicReference to update a shared object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.concurrent.atomic.AtomicReference;

public class SharedObject {
private String value;

public SharedObject(String value) {
this.value = value;
}

public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value;
}
}

public class OptimisticLockingExample {
private AtomicReference<SharedObject> sharedObject = new AtomicReference<>(new SharedObject("initial value"));

public void updateSharedObject(String newValue) {
SharedObject currentObject = sharedObject.get();
SharedObject newObject = new SharedObject(newValue);

if (sharedObject.compareAndSet(currentObject, newObject)) {
System.out.println("Updated shared object to: " + newValue);
} else {
System.out.println("Failed to update shared object. Another thread modified it.");
}
}
}

In this example, the updateSharedObject method attempts to update the shared object with a new value. If another thread has modified the object in the meantime, the compareAndSet method will fail, and the update will not be applied. This is a form of optimistic locking that avoids blocking threads and waiting for locks. The failed update can be retried or handled in other ways, depending on the application’s requirements.

Atomic Variables in Collections

Atomic variables can be used in collections to provide atomic operations on the collection as a whole. For example, AtomicReference can be used to store a reference to a list of items, allowing atomic updates to the list. Here’s an example of a simple cache implementation using AtomicReference to store a list of items.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

public class SimpleCache {
private final AtomicReference<List<String>> cachedItems = new AtomicReference<>();

public boolean setCachedItemsIf(List<String> expectedItems, List<String> newItems) {
return cachedItems.compareAndSet(expectedItems, newItems);
}

public void setCachedItems(List<String> items) {
cachedItems.set(items);
}

public List<String> getCachedItems() {
return cachedItems.get();
}

public void clearCache() {
cachedItems.set(null);
}
}

In this example, the SimpleCache class uses an AtomicReference to store a list of items. The setCachedItemsIf method attempts to update the cached items only if the expected items match the current items. This ensures that the update is atomic. setCachedItems and clearCache methods provide simple operations to set and clear the cache.

Atomic Variables VS Volatile

Atomic variables provide atomic operations on a single variable, ensuring that updates are visible to all threads. They are suitable for simple shared state management.
Volatile variables provide visibility guarantees for reads and writes of a single variable, ensuring that changes made by one thread are visible to others. They are useful for simple flags and status variables.
Atomic variables are more powerful than volatile variables, as they provide atomic operations like compare-and-swap, increment, decrement, and set. They are suitable for more complex shared state management.
Volatile variables are limited to visibility guarantees and do not provide atomic operations. They are suitable for simple flags and status variables where atomicity is not required.
Atomic variables are preferred when multiple threads need to perform atomic operations on a shared variable, while volatile variables are suitable for simple visibility guarantees.

In the previous UnsafeCounter example, we used AtomicInteger to provide atomic operations for incrementing and getting the counter value. If we had used a volatile int instead, the counter would not have been thread-safe, as volatile only provides visibility guarantees and does not ensure atomicity.

Conclusion

In essence, atomic variables are a lightweight and efficient way to manage simple shared state in concurrent applications. They provide better performance than locks in many scenarios, but they are not a replacement for all synchronization needs. Choosing the right approach depends on the specific requirements of the application, considering factors like the complexity of operations, the level of contention, and performance requirements.