Зміст курсу
Multithreading in Java
Multithreading in Java
Atomic 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.
Main
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.
Main
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.
Main
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.
Main
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.
Main
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.
Main
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.
Дякуємо за ваш відгук!