Notice: This page requires JavaScript to function properly.
Please enable JavaScript in your browser settings or update your browser.
Atomic Variables | High-level Synchronization Mechanisms
Multithreading in Java
course content

Course Content

Multithreading in Java

Multithreading in Java

1. Multithreading Basics
2. Synchronized Collections
3. High-level Synchronization Mechanisms
4. Multithreading Best Practices

bookAtomic Variables

We’ve already covered what atomicity is and the problems it can cause in the first section of this course. Back then, we addressed the issue using synchronized blocks or methods. Now, we’ll explore how to achieve the same result more easily by using an atomic class.

What are Atomic Variables?

Atomic variables ensure that operations (read, write, increment) on variables are performed atomically, meaning they are executed contiguously and safely in a multithreaded environment. This guarantees that the operation will be completed entirely, without the possibility of interference from other threads during its execution.

Why do We Need Atomic Variables?

Without the use of atomic variables or other synchronization mechanisms, operations like increment (++) can be unsafe. For instance, when multiple threads access the same variable simultaneously, updates might be lost, resulting in incorrect results. Atomic variables solve this problem by ensuring that operations on them are executed sequentially.

We previously discussed this issue when the increment operation was broken down into three steps (read, increment, write), but with atomic variables, it’s all done in one operation!

Types of Atomic Variables in Java

Note

In general, there are many atomic implementations, and we won’t cover them all here, as it would take too long.

Java provides several atomic variable classes in the java.util.concurrent.atomic package, each designed to handle a specific data type:

  • AtomicInteger: for atomic operations on int;
  • AtomicLong: for atomic operations on long;
  • AtomicBoolean: for atomic operations on boolean;
  • AtomicReference<V>: for atomic operations on objects (generic type).

Methods

The get() method returns the current value of a variable. The set(V newValue) method sets a new value for the variable. On the other hand, lazySet(V newValue) is similar to set(), but it can defer updating the value, offering an ordered update in certain situations.

java

Main

copy
123456789101112131415161718192021
package com.example; import java.util.concurrent.atomic.AtomicReference; public class Main { public static void main(String[] args) { AtomicReference<String> atomicString = new AtomicReference<>("Initial Value"); // Using `get()` to retrieve the current value String value = atomicString.get(); System.out.println("Current Value: " + value); // Using `set()` to update the value atomicString.set("New Value"); System.out.println("Value after set(): " + atomicString.get()); // Using `lazySet()` to update the value atomicString.lazySet("Lazy Set Value"); System.out.println("Value after lazySet(): " + atomicString.get()); } }

The compareAndSet(V expect, V update) method updates the value if the current value matches the expected value. It returns true if the update was successful, and false if the current value did not match the expected value. In contrast, the getAndSet(V newValue) method sets a new value and returns the previous value.

java

Main

copy
123456789101112131415161718192021222324
package com.example; import java.util.concurrent.atomic.AtomicReference; public class Main { public static void main(String[] args) { // Initialize an `AtomicReference` with an initial value AtomicReference<String> atomicString = new AtomicReference<>("Initial Value"); // Demonstrate `compareAndSet` boolean success = atomicString.compareAndSet("Initial Value", "Updated Value"); System.out.println("compareAndSet success (expected true): " + success); System.out.println("Current value after compareAndSet: " + atomicString.get()); success = atomicString.compareAndSet("Wrong Value", "Another Update"); System.out.println("compareAndSet success (expected false): " + success); System.out.println("Current value after compareAndSet: " + atomicString.get()); // Demonstrate `getAndSet` String previousValue = atomicString.getAndSet("New Value with getAndSet"); System.out.println("Previous value from getAndSet: " + previousValue); System.out.println("Current value after getAndSet: " + atomicString.get()); } }

The getAndIncrement() and getAndDecrement() methods increment or decrement the current value by one and return the previous value. These methods apply to numeric atomic variables such as AtomicInteger and AtomicLong. In contrast, the incrementAndGet() and decrementAndGet() methods also increment or decrement the current value by one but return the new value.

java

Main

copy
123456789101112131415161718192021222324252627282930
package com.example; import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { // Initialize `AtomicInteger` with initial value AtomicInteger atomicInt = new AtomicInteger(10); // Demonstrate `getAndIncrement()` for `AtomicInteger` int oldValueInt = atomicInt.getAndIncrement(); System.out.println("Value before getAndIncrement(): " + oldValueInt); // Should print 10 System.out.println("Value after getAndIncrement(): " + atomicInt.get()); // Should print 11 // Demonstrate `getAndDecrement()` for `AtomicInteger` int oldValueIntDec = atomicInt.getAndDecrement(); System.out.println("Value before getAndDecrement(): " + oldValueIntDec); // Should print 11 System.out.println("Value after getAndDecrement(): " + atomicInt.get()); // Should print 10 // Demonstrate `incrementAndGet()` for `AtomicInteger` int newValueInt = atomicInt.incrementAndGet(); System.out.println("Value after incrementAndGet(): " + newValueInt); // Should print 11 System.out.println("Current value after incrementAndGet(): " + atomicInt.get()); // Should print 11 // Demonstrate `decrementAndGet()` for `AtomicInteger` int newValueIntDec = atomicInt.decrementAndGet(); System.out.println("Value after decrementAndGet(): " + newValueIntDec); // Should print 10 System.out.println("Current value after decrementAndGet(): " + atomicInt.get()); // Should print 10 } }

The getAndAdd(int delta) method adds the specified value (delta) to the current value and returns the previous value. This method is used with numeric atomic variables. On the other hand, the addAndGet(int delta) method also adds the specified value (delta) to the current value but returns the new value.

java

Main

copy
123456789101112131415161718192021222324252627282930
package com.example; import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { // Initialize `AtomicInteger` with an initial value AtomicInteger atomicInt = new AtomicInteger(50); // Demonstrate `getAndAdd(int delta)` int previousValue = atomicInt.getAndAdd(10); System.out.println("Value before getAndAdd(10): " + previousValue); // Should print 50 System.out.println("Value after getAndAdd(10): " + atomicInt.get()); // Should print 60 // Demonstrate `getAndAdd()` with another delta int previousValue2 = atomicInt.getAndAdd(5); System.out.println("Value before getAndAdd(5): " + previousValue2); // Should print 60 System.out.println("Value after getAndAdd(5): " + atomicInt.get()); // Should print 65 // Demonstrate `addAndGet(int delta)` int newValue = atomicInt.addAndGet(20); System.out.println("Value after addAndGet(20): " + newValue); // Should print 85 System.out.println("Current value after addAndGet(20): " + atomicInt.get()); // Should print 85 // Demonstrate `addAndGet()` with another delta int newValue2 = atomicInt.addAndGet(-15); System.out.println("Value after addAndGet(-15): " + newValue2); // Should print 70 System.out.println("Current value after addAndGet(-15): " + atomicInt.get()); // Should print 70 } }

Examples of Using Atomic Variables

Example with AtomicInteger (AtomicLong, AtomicBoolean are the same, so there will be no separate examples for them).

Task: To implement a counter that is safely incremented by several threads.

java

Main

copy
1234567891011121314151617181920
package com.example; import java.util.concurrent.atomic.AtomicInteger; public class Main { private AtomicInteger counter = new AtomicInteger(0); public void increment() { int oldValue = counter.getAndIncrement(); System.out.println(Thread.currentThread().getName() + ": Counter was " + oldValue + ", now " + counter.get()); } public static void main(String[] args) { Main atomicCounter = new Main(); for (int i = 0; i < 5; i++) { new Thread(atomicCounter::increment).start(); } } }

As you can see we didn't use any synchronization here, as the atomic variable itself provides this function

This example uses AtomicInteger to safely increment the counter. The getAndIncrement() method first returns the current value of the variable, then increments it by one. This happens atomically, ensuring that the variable is updated correctly.

Example with AtomicReference

Task: Provide atomic updating of a reference to an object.

java

Main

copy
123456789101112131415161718192021222324
package com.example; import java.util.concurrent.atomic.AtomicReference; public class Main { // `AtomicReference` to safely update and access a shared `String` value private AtomicReference<String> sharedString = new AtomicReference<>("Initial"); public void updateValue(String newValue) { // Atomically sets the new value and gets the old value String oldValue = sharedString.getAndSet(newValue); // Prints the old and new values, along with the thread name System.out.println(Thread.currentThread().getName() + ": Value was " + oldValue + ", now " + sharedString.get()); } public static void main(String[] args) { Main example = new Main(); // Creates and starts 3 threads, each updating the shared value for (int i = 0; i < 3; i++) { new Thread(() -> example.updateValue("Updated by " + Thread.currentThread().getName())).start(); } } }

AtomicReference is used here to atomically update the value of a string reference. The getAndSet() method atomically sets the new value and returns the previous value.

Note

Unlike regular variables, which require additional *synchronization, atomic variables utilize low-level primitives to minimize overhead and enhance performance. This makes them particularly well-suited for high-concurrency systems.

1. What is the advantage of using atomic variables in multithreaded programming?
2. Which atomic variable method provides an atomic change in value if the current value is the same as the expected value?
3. What does the set() method guarantee in atomic variables?
What is the advantage of using atomic variables in multithreaded programming?

What is the advantage of using atomic variables in multithreaded programming?

Select the correct answer

Which atomic variable method provides an atomic change in value if the current value is the same as the expected value?

Which atomic variable method provides an atomic change in value if the current value is the same as the expected value?

Select the correct answer

What does the set() method guarantee in atomic variables?

What does the set() method guarantee in atomic variables?

Select the correct answer

Everything was clear?

How can we improve it?

Thanks for your feedback!

Section 3. Chapter 5
some-alt