Atomicity Issues in Multithreading and Their Solutions
Atomicity
An atomic operation is one that is executed as a single, indivisible unit. The operation either completes entirely or not at all.
Atomicity Issues in Multithreading
Consider a scenario where we want 100 threads to each deliver 100 flowers to Xiao Ha, but we find that the total is less than 10,000 flowers.
Root Cause Analysis
The count++ operation consists of the following steps:
- The thread copies the shared variable to its stack
- The thread increments the copied value
- The thread assigns the copied value back to the shared variable in memory
If Thread A is between steps 2 and 3 when Thread B performs step 1, then both threads will increment count from the same initial value. This results in count being incremented twice from the same base value, meaning one flower was not delivered.
Example Code
public class FlowerDelivery implements Runnable {
private int count = 0;
@Override
public void run() {
for(int i = 0; i < 100; i++) {
count++;
System.out.println("Delivering flower #" + count + " to Xiao Ha");
}
}
}
public class DeliveryTest {
public static void main(String[] args) {
FlowerDelivery deliveryTask = new FlowerDelivery();
for(int i = 0; i < 100; i++) {
new Thread(deliveryTask).start();
}
}
}
Solution 1: AtomicInteger
The AtomicInteger class provides atomic operations that ensure the consistency of count++.
import java.util.concurrent.atomic.AtomicInteger;
public class FlowerDelivery implements Runnable {
private AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println("Delivering flower #" + count.incrementAndGet() + " to Xiao Ha");
}
}
}
public class DeliveryTest {
public static void main(String[] args) {
FlowerDelivery deliveryTask = new FlowerDelivery();
for(int i = 0; i < 100; i++) {
new Thread(deliveryTask).start();
}
}
}
Solution 2: synchronized
Use synchronized blocks to lock the code that accesses shared resources, ensuring only one thread can operate on the shared data at a time.
public class FlowerDelivery implements Runnable {
private int count = 0;
@Override
public void run() {
synchronized(this) {
for(int i = 0; i < 100; i++) {
count++;
System.out.println("Delivering flower #" + count + " to Xiao Ha");
}
}
}
}
public class DeliveryTest {
public static void main(String[] args) {
FlowerDelivery deliveryTask = new FlowerDelivery();
for(int i = 0; i < 100; i++) {
new Thread(deliveryTask).start();
}
}
}
Principle Behind AtomicInteger
Spin + CAS Algorithm
The CAS algorithm involves three operands:
- Memory value: The shared data to be modified
- Expected value: The value read from memory
- New value: The result after performing the operation on the expected value
The algorithm works as follows:
- If the expected value matches the current memory value, it means no other thread has modified the shared data. In this case, the memory value is updated to the new value, and the operation succeeds.
- If the expected value doesn't match the current memory value, it means another thread has modified the shared data. In this case, the operation fails, and a new value is read from memory to retry the process. This retry mechanism is called spinning.