🔹 What is Memory Visibility?
Memory visibility refers to how changes made by one thread become visible to other threads in a multi-threaded environment. In Java, each thread may have its own local cache of variables, which can lead to unexpected behavior when multiple threads access and modify shared data.
💡 The Problem: Cached Variables
- CPU caches improve performance by storing frequently used variables.
- Each thread may use its own copy of a variable instead of the latest value from main memory.
- Changes made by one thread may not be visible to others due to caching.
🚀 Example 1: Memory Visibility Issue Without volatile
❌ Expected Behavior
- The main thread starts a worker thread.
- The worker thread runs a loop while isRunning is true.
- After 1 second, the main thread sets isRunning = false.
- The worker thread should stop when it sees isRunning is false.
public class VisibilityIssue {
public static void main(String[] args) {
Task task = new Task();
Thread worker = new Thread(task, "WorkerThread");
worker.start();
try {
Thread.sleep(1000); // Let the worker thread run for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread: Changing isRunning to false...");
task.isRunning = false; // Attempt to stop the worker thread
}
static class Task implements Runnable {
boolean isRunning = true; // Shared variable (no volatile)
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " started...");
while (isRunning) { // The thread should stop when isRunning becomes false
}
System.out.println(Thread.currentThread().getName() + " stopped.");
}
}
}
🔹 Expected Output
WorkerThread started...
Main thread: Changing isRunning to false...
WorkerThread stopped.
🔴 Actual Output (Problem)
WorkerThread started...
Main thread: Changing isRunning to false...
- The worker thread does not stop because it never sees the updated value of isRunning.
- The worker thread continues to use the cached value (true) in its CPU instead of fetching the updated value (false) from memory.
🚀 Example 2: Fixing Memory Visibility with volatile
To fix this issue, we use the volatile keyword, which ensures:
- All reads and writes go directly to main memory.
- All threads see the most up-to-date value.
public class VisibilityFixed {
public static void main(String[] args) {
Task task = new Task();
Thread worker = new Thread(task, "WorkerThread");
worker.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread: Changing isRunning to false...");
task.isRunning = false; // Now the change is visible to all threads
}
static class Task implements Runnable {
volatile boolean isRunning = true; // Using volatile ensures visibility
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " started...");
while (isRunning) {
}
System.out.println(Thread.currentThread().getName() + " stopped.");
}
}
}
✅ Corrected Output
WorkerThread started...
Main thread: Changing isRunning to false...
WorkerThread stopped.
Why Does volatile Fix the Issue?
- volatile ensures that every read of isRunning happens from main memory, not cache.
- When isRunning is updated in the main thread, the change is immediately visible to the worker thread.
🚀 Example 3: Real-Time Counter Without volatile (Incorrect)
public class CounterIssue {
public static void main(String[] args) {
CounterTask task = new CounterTask();
Thread worker = new Thread(task, "WorkerThread");
worker.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread: Changing active to false...");
task.active = false;
System.out.println("Final counter value: " + task.counter);
}
static class CounterTask implements Runnable {
boolean active = true;
long counter = 0;
@Override
public void run() {
while (active) {
counter++; // Updating shared variable
}
System.out.println("WorkerThread stopped. Final counter: " + counter);
}
}
}
🔹 Issue
Even though active is false in the main thread, the worker thread continues running due to caching.
🚀 Example 4: Fixing with volatile (Still Has an Issue with Atomicity)
import java.util.concurrent.atomic.AtomicLong;
public class CounterFixed {
public static void main(String[] args) {
CounterTask task = new CounterTask();
Thread worker = new Thread(task, "WorkerThread");
worker.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread: Changing active to false...");
task.active = false;
System.out.println("Final counter value: " + task.counter);
}
static class CounterTask implements Runnable {
volatile boolean active = true;
// ❌ Incorrect: volatile does not guarantee atomicity for increments
// volatile long counter = 0;
// ✅ Correct: Use AtomicLong for atomic operations
AtomicLong counter = new AtomicLong(0);
@Override
public void run() {
while (active) {
counter.incrementAndGet(); // Ensures atomicity
}
System.out.println("WorkerThread stopped. Final counter: " + counter.get());
}
}
}
⚠️ Important Note: volatile Does NOT Guarantee Atomicity
- volatile long counter = 0; ensures visibility but not atomicity.
- Why? The counter++ operation consists of three steps:
- Read the current value.
- Increment the value.
- Write the new value back.
- If multiple threads execute counter++, race conditions can still occur, leading to lost updates.
- Solution: Use AtomicLong instead, which provides atomic operations like incrementAndGet().
✅ Corrected Output
Now, the counter increments correctly without race conditions.
🔹 When to Use volatile vs. Atomic Variables?
Use Case volatile Atomic Variables (AtomicLong, AtomicInteger)
Thread visibility | ✅ Ensures visibility across threads | ✅ Ensures visibility |
Atomic operations (++, --, +=, -=) | ❌ Not safe! Multiple threads may overwrite each other's updates | ✅ Safe! Provides atomicity |
Performance impact | ⚡ Faster (no locking) | 🐢 Slightly slower (ensures atomicity) |
Use for flags (boolean) | ✅ Yes, perfect for simple flags | 🚫 Not needed |
Use for counters (long, int) | ❌ No, use AtomicLong or synchronization | ✅ Yes, prevents race conditions |
🔹 Key Takeaways
Concept Without volatile With volatile
Thread visibility | Changes may not be seen by other threads | Changes are immediately visible |
Caching issue | Threads may use stale data from cache | Always reads/writes to main memory |
Performance | Faster (due to caching) | Slightly slower (but safer for visibility) |
Atomicity | ❌ No | ❌ No (Still needs synchronized for atomic operations) |
'JAVA' 카테고리의 다른 글
LockSupport & ReentrantLock (0) | 2025.03.10 |
---|---|
synchronized (0) | 2025.03.10 |
interrupt() in java threads (0) | 2025.03.09 |
join() in java thread (0) | 2025.03.09 |
Why Can't run() Throw Checked Exceptions? (0) | 2025.03.08 |