In concurrent programming, simple read and write operations are often not enough. When multiple threads or coroutines interact with shared state, we need stronger guarantees to ensure correctness.
AtomicFU provides a set of atomic primitives that help us manage shared state safely, without using locks. These primitives are designed to be lightweight and efficient, and they work across Kotlin/JVM, Kotlin/Native, and Kotlin/JS.
In this topic, we'll explore several advanced atomic operations provided by AtomicFU, such as:
compareAndSetto update a value only if it matches an expected valueupdate,updateAndGet,getAndUpdatefor lambda-based updatesloopto implement retry logic based on the current valueatomic arrays to manage multiple atomic values in a collection
Each of these tools gives you more control over how atomic state is modified in concurrent scenarios.
Conditional updates with compareAndSet
Sometimes, we don’t just want to update a value: we want to update it only if it currently holds a specific value. That’s where compareAndSet comes in.
This operation checks whether the atomic variable has a particular expected value, and if so, replaces it with a new one. The whole check-and-update happens atomically, making it ideal for implementing fine-grained control over state transitions.
Here’s the basic usage:
val flag = atomic(false)
if (flag.compareAndSet(false, true)) {
println("We flipped the flag!")
} else {
println("Someone else changed it first.")
}In this example, the update succeeds only if flag is false at the moment of the call. If it was already true, nothing happens, and the function returns false.
A more practical use case is one-time initialization. Suppose we want to ensure a resource is initialized only once, even if multiple threads are racing to do it:
val initialized = atomic(false)
fun initialize() {
if (initialized.compareAndSet(false, true)) {
// This block will only run once
println("Initializing the resource...")
// perform initialization here
} else {
println("Already initialized by another thread.")
}
}This pattern helps prevent redundant work and is especially useful in concurrent systems where multiple components might attempt to perform the same setup.
Update atomic value with update
The update function provided by AtomicFU is a fundamental tool for modifying the value of an atomic variable in a safe and elegant way. Unlike direct writes, which may lead to race conditions when multiple threads access the same variable, update guarantees that each change happens atomically, without any risk of interference.
The idea is straightforward: we pass a lambda that receives the current value and returns the new value. AtomicFU takes care of all the synchronization behind the scenes. If another thread updates the variable before our operation completes, the lambda is automatically retried with the latest value. This ensures correctness even under heavy concurrency, without requiring us to write complex locking logic.
Here’s a simple example where update is used to increment a counter:
val counter = atomic(0)
counter.update { current ->
current + 1
}This increments counter by one in a thread-safe way. Even if multiple threads run this code simultaneously, every increment will be accounted for and no updates will be lost.
A practical use case might be tracking how many users are currently active in our system. We could write:
val activeUsers = atomic(0)
fun userLoggedIn() {
activeUsers.update { it + 1 }
}
fun userLoggedOut() {
activeUsers.update { it - 1 }
}The update function is a great choice when we want to transform the current value based on some logic but don’t need to know the previous or new value as a result. If we do need the result, AtomicFU provides other variants like updateAndGet and getAndUpdate.
How it works internally
Internally, update repeatedly attempts to update the value using a compare-and-set loop. It reads the current value, applies our lambda to compute a new one, and tries to update the atomic variable using compareAndSet.
If the value has changed in the meantime (i.e., another thread updated it), the lambda is re-executed with the latest value. This retry mechanism continues until the update succeeds.
We don’t need to worry about retries: AtomicFU handles them automatically. But it’s important to remember that our lambda may be executed more than once, so it must be free of side effects.
Compared to compareAndSet , which requires us to check the current value manually and decide what to do if the update fails, update provides a higher-level abstraction. It's ideal when we want the update to succeed eventually, without writing custom retry logic.
Complex updates
In addition to update, AtomicFU provides a few variations that make certain use cases more convenient. Specifically, updateAndGet and getAndUpdate let us perform an atomic update and return either the new or the previous value, depending on our needs.
With updateAndGet, the lambda is applied to the current value, and the result becomes the new value which is also returned by the function:
val result = atomicInt.updateAndGet { it * 2 }Here, result holds the new value after doubling the current one.
On the other hand, getAndUpdate returns the original value before applying the update:
val old = atomicInt.getAndUpdate { it + 10 }This can be useful when we care about what the value was just before the update occurred, such as when implementing counters or queues.
For basic arithmetic operations, AtomicFU also provides operator overloads that make the syntax more concise. For example:
val counter = atomic(0)
counter += 1
counter -= 2These lines are equivalent to using update, but offer a cleaner and more idiomatic Kotlin style for simple increments or decrements.
All of these operations maintain the same level of thread safety as update, and internally follow a similar retry mechanism based on compareAndSet. They are just tailored for different patterns of usage, helping us write clearer and more intention-revealing code.
Volatile reads with loop
Sometimes, we need more control over how updates are applied, especially when they depend on a condition based on the current value. In these cases, AtomicFU provides the loop method.
The loop { current -> ... } function repeatedly invokes our lambda with the latest value of the atomic variable. If the update fails (because another thread modified the value in the meantime), the lambda is retried with the new value.
Here's a simple example:
val flag = atomic(0)
flag.loop { current ->
if (current < 10) {
current + 1
}
else return
}In this case, the value is incremented only if it’s less than 10. If it’s already 10 or higher, the lambda returns, which tells the loop to stop without updating anything.
This is especially useful when the logic we're applying must be based on the current value and should be retried if that value changes in the meantime, something we can’t do safely with just update.
Let’s look at another practical scenario: a stock counter that can only be decreased if stock is available.
val stock = atomic(10)
stock.loop { current ->
if (current > 0) {
}return@loop current - 1
else return@loop break
}
println(stock.value) // 9This example shows how loop ensures that only one thread (or coroutine) can successfully decrease the value at a time. If multiple threads try to decrement simultaneously, only one will succeed, and the others will retry based on the most recent value.
By combining retries with conditional logic, loop provides a safe and flexible way to implement atomic operations that depend on dynamic state.
Atomic arrays
In some applications, we might need to manage multiple atomic values together, for example, tracking counts or flags in a fixed-size collection. Instead of creating separate atomic variables, AtomicFU offers atomic arrays, which allow us to safely operate on individual elements in an array-like structure.
We can create atomic arrays of integers, longs, or references. Here's how to define an atomic integer array:
val counters = AtomicIntArray(5)
counters[0].incrementAndGet()
counters[1].update { it + 1 }Each element in the array is a separate atomic value, and we can perform any atomic operation on it, just like we would on a standalone atomic variable.
AtomicFU provides similar array types for other data as well. We can use AtomicLongArray for long values,AtomicBooleanArray for boolean values or AtomicArray<T> for references. The latter is especially useful when we want to manage a list of objects or nullable elements atomically.
And here’s an example using references to a custom data class:
data class Task(val id: Int, val completed: Boolean)
val tasks = AtomicArray<Task>(size = 2)
tasks[0].value = Task(1, false)
tasks[0].update { it.copy(completed = true) }In this case, we can update complex objects safely and atomically without locks, by replacing or modifying the entire reference at a specific index.
Each element in these arrays behaves independently, so updates to one index don't interfere with the others. Whether we're incrementing counters, flipping flags, or updating objects, all operations remain thread-safe and non-blocking.
Atomic arrays offer a clean and efficient way to handle such scenarios while preserving the same atomic guarantees we get with single atomic variables. Whether we're building a statistics collector, a concurrent scheduler, or a shared buffer, atomic arrays give us the flexibility to scale our logic safely across multiple values.
Conclusion
AtomicFU provides a powerful set of tools for managing shared state in concurrent programs. Beyond basic atomic reads and writes, the library offers advanced operations like update, compareAndSet, and loop, giving us fine-grained control over how values are modified under contention.
We’ve seen how these operations work with individual atomic variables, as well as how they scale to collections through atomic arrays. Whether we're incrementing counters, coordinating tasks, or conditionally updating values, AtomicFU helps us write safe, lock-free code that's both expressive and efficient.
By understanding the tools AtomicFU provides and how to combine them, we’ll be better equipped to build concurrent applications that are robust, scalable, and easy to reason about.