Volatile in Java

2023 年 2 月 22 日 星期三(已编辑)
34
摘要
The main purpose of volatile is to ensure visibility and ordering of changes to a variable across threads.

Volatile in Java

In Java, the volatile keyword is used to mark a variable as "volatile", which indicates that its value may be modified by multiple threads simultaneously. The main purpose of volatile is to ensure visibility and ordering of changes to a variable across threads.

Here’s an explanation of what volatile does and when to use it:

1 Visibility Guarantee

When a variable is marked as volatile, it guarantees that any changes to its value will be immediately visible to all threads. Without volatile, the Java Memory Model (JMM) allows each thread to keep a local copy of variables, which can lead to stale values being read in a multi-threaded environment.

Example:

public class SharedData {
    private volatile boolean flag = false;

    public void changeFlag() {
        flag = true;  // This change will be visible to all threads
    }

    public boolean checkFlag() {
        return flag;  // Always returns the latest value of flag
    }
}

In this example, if flag was not declared volatile, one thread could update flag to true, but other threads might still see it as false due to caching.

2 Avoiding Caching and Reordering

The volatile keyword prevents the JVM from caching the variable or reordering instructions related to the variable. Without volatile, the compiler, processor, or runtime environment may reorder instructions for optimization, which can lead to subtle bugs in multi-threaded programs.

When a variable is declared volatile, the following happens:

  • Reads of the volatile variable always read from the main memory (instead of from thread-local cache).
  • Writes to the volatile variable are always written to the main memory, making the change visible to other threads immediately.
  • Reordering prevention: Any read or write before a volatile write or after a volatile read cannot be reordered by the JVM or processor.

Here’s an example where volatile is used to avoid reording instructions:

/**
* A Singleton Class with Thread Safety
* Pros:
* 1. Thread safety is guaranteed
* 2. Client application can pass arguments
* 3. Lazy initialization achieved
* 4. Synchronization overhead is minimal and applicable only for first few threads when the variable is null
* Cons:
* 1. Extra if condition
*/
public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return result;
    }
}

It is necessary to use the volatile keyword to modify uniqueInstance. The code uniqueInstance = new Singleton(); is actually executed in three steps:

  1. Allocate memory space for uniqueInstance.
  2. Initialize uniqueInstance.
  3. Point uniqueInstance to the allocated memory address.

However, due to the JVM’s instruction reordering feature, the execution order may become 1 -> 3 -> 2. Instruction reordering does not cause issues in a single-threaded environment, but in a multi-threaded environment, it can result in one thread obtaining an instance that hasn’t been initialized yet. For example, if thread T1 executes steps 1 and 3, and at that moment thread T2 calls getUniqueInstance(), it will find that uniqueInstance is not null and will return it, but uniqueInstance has not yet been initialized.

Note

For more information about Java Memory Model, please refer to this article Java Memory Model

3 When to Use volatile

  • When multiple threads are reading and writing to a shared variable: Use volatile to ensure that changes made by one thread are visible to all other threads.
  • When you don’t need atomicity: volatile is useful for ensuring visibility but doesn’t guarantee atomicity (which means that complex operations like incrementing a variable are not thread-safe). For atomic operations, use synchronization or classes like AtomicInteger.

4 Volatile Does Not Ensure Atomicity

While volatile guarantees visibility, it does not ensure that operations on the variable are atomic. For instance, the following increment operation is not thread-safe, even if count is volatile:

private volatile int count;

public void increment() {
    count++;  // This is not thread-safe even though count is volatile
}

In this example, the increment (count++) involves three operations: read, increment, and write. These operations are not atomic, so if multiple threads execute increment(), the final value of count could be incorrect due to race conditions. To ensure atomicity in this case, use synchronized or AtomicInteger or ReentrantLock.

// synchronized
public synchronized void increase() {
    inc++;
}

// AtomicInteger
public AtomicInteger inc = new AtomicInteger();

public void increase() {
    inc.getAndIncrement();
}

// ReentrantLock
Lock lock = new ReentrantLock();
public void increase() {
    lock.lock();
    try {
        inc++;
    } finally {
        lock.unlock();
    }
}

5 Volatile vs. Synchronization

  • volatile provides a lightweight mechanism for ensuring visibility and ordering of changes to a variable across threads. It is faster than using synchronized because it doesn’t block or provide mutual exclusion.
  • synchronized provides visibility and also guarantees atomicity by allowing only one thread at a time to execute a block of code. It also forces a happens-before relationship between threads.

6 Example of Correct Use of Volatile

Here’s an example where volatile is sufficient:

class StopThread {
    private volatile boolean running = true;

    public void run() {
        while (running) {
            // Do some work
        }
    }

    public void stop() {
        running = false;  // This change will be visible to the thread running the loop
    }
}

In this example, volatile is used to ensure that the running variable is properly updated across multiple threads. One thread can safely stop the execution of the loop in another thread without needing synchronized.

Summary of Volatile Characteristics:

  • Visibility: Changes to a volatile variable are immediately visible to all threads.
  • Prevents reordering: Memory operations involving volatile variables are not reordered.
  • No atomicity: Volatile does not make compound actions (like incrementing) atomic.
  • Faster than synchronized: Volatile is less expensive than locking mechanisms but does not provide mutual exclusion.

In summary, the volatile keyword is useful for ensuring visibility and ordering in concurrent programming but should be used carefully and only when synchronization is not required.

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...