刘耀文

刘耀文

java开发者
github

Understanding the volatile keyword and instruction reordering in Java

In multithreaded programming, the volatile keyword is an important tool to ensure the visibility of variables between multiple threads and also helps prevent instruction reordering. Below, we will explain these concepts in detail and illustrate the usage scenarios and limitations of volatile through practical examples.

Visibility

In a multithreaded environment, a modification of a shared variable by one thread may not be immediately visible to other threads. This is because a thread may cache the value of the variable instead of reading it directly from the main memory. For example:

class SharedObject {
    private boolean flag = false;

    public void setFlag() {
        this.flag = true;
    }

    public boolean getFlag() {
        return this.flag;
    }
}

Suppose thread A calls the setFlag() method to set flag to true, while thread B calls the getFlag() method to check the value of flag. Without using volatile, thread B may not see the modification of flag by thread A because the update of flag may only exist in the cache of thread A and has not been synchronized to the main memory.

How volatile solves the visibility problem

By declaring a variable as volatile, it ensures that all modifications to this variable are visible to all threads:

class SharedObject {
    private volatile boolean flag = false;

    public void setFlag() {
        this.flag = true;
    }

    public boolean getFlag() {
        return this.flag;
    }
}

In this example, flag is declared as volatile, which means that every write operation to flag will immediately update the main memory, and any thread reading the value of flag will directly get the latest value from the main memory, thus ensuring the visibility of the variable.

Limitations of volatile

Although volatile ensures visibility, it does not guarantee atomicity of operations. Atomicity means that an operation either succeeds completely or fails completely. For example, the count++ operation in the following code is not atomic:

class Counter {
    private volatile int count = 0;

    public void increment() {
        count++;
    }
}

The count++ operation actually consists of three steps:

  1. Read the value of count.
  2. Increment the value.
  3. Write the value back to count.

If two threads execute the increment() method simultaneously, they may read the same value of count, then increment it separately, resulting in the value of count being less than the actual number of increments.

How to ensure atomicity

To ensure atomicity of operations, synchronized or the AtomicInteger class can be used:

Using synchronized:

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

Using synchronized ensures that only one thread can execute the increment() method at a time, thus ensuring the atomicity of the count++ operation.

Using AtomicInteger:

import java.util.concurrent.atomic.AtomicInteger;

class Counter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }
}

AtomicInteger provides atomic operations such as incrementAndGet(), which can be safely used for concurrent operations.

Instruction Reordering

Instruction reordering is the adjustment of the execution order of code instructions by the compiler and CPU to optimize performance. This may lead to unexpected behavior in a multithreaded environment. For example:

class Example {
    private int x = 0;
    private boolean flag = false;

    public void method1() {
        x = 1;
        flag = true;
    }

    public void method2() {
        if (flag) {
            System.out.println(x);
        }
    }
}

Without using volatile, the compiler or CPU may reorder the execution order of flag = true and x = 1, which may result in flag being set to true in method2 but x not being updated yet.

How volatile handles instruction reordering

The volatile keyword prevents instruction reordering of volatile variables. After using the keyword, write operations will not be reordered before read operations, and read operations will not be reordered after write operations, thus avoiding the problems caused by instruction reordering:

class Example {
    private volatile int x = 0;
    private volatile boolean flag = false;

    public void method1() {
        x = 1;
        flag = true;
    }

    public void method2() {
        if (flag) {
            System.out.println(x);
        }
    }
}

In this example, both flag and x are declared as volatile, which ensures that flag = true in method1 will not be reordered before x = 1, so that x can be correctly read in method2.

In a nutshell

volatile is an important tool in Java to ensure the visibility of variables among multiple threads and prevent instruction reordering. However, it does not guarantee atomicity of operations. In scenarios where atomicity needs to be ensured, consider using synchronized or AtomicInteger.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.