Deep Analysis of volatile Keyword and the Use of Atomic Integer

Keywords: Java less

Semantics of volatile:
1. Ensure the visibility of volatile-modified variables to all other threads.
2. Variables modified with volatile prohibit instruction rearrangement optimization.
Look at the code:

public class InheritThreadClass extends Thread{
    private static volatile int a = 0;
    @Override
    public void run() {
        for(int i = 0; i < 1000; i++){
            a++;
        }
    }
    public static void main(String[] args) {
        InheritThreadClass[] threads = new InheritThreadClass[100];
        for(int i=0; i < 100; i++){
            threads[i] = new InheritThreadClass();
            threads[i].start();
        }
        //Waiting for all sub-threads to end
        while(Thread.activeCount() > 1){
            Thread.yield();
        }
        
        //This code will be executed after all the sub-threads have finished running.
        System.out.println(a);  //(1)
    }
}

The above code creates 100 threads, and then increments variable a 1000 times in each thread, which means that if the code can run concurrently correctly, it should output 100000 at code (1). But running many times, you will find that the output of each time is not what we expected, but is less than or equal to 100,000. That is to say, the results of each run are not fixed and different. Why? Because we know from the semantics of the volatile keyword above that the variables modified by the keyword are visible to all threads, how can this happen? Is there any semantic error? That's impossible. There must be no mistake in semantics.

We know that every thread has its own private memory, and the communication between threads is achieved through main memory. Volatile guarantees multithreading visibility here. Volatile means that if a thread modifies a variable modified by volatile keyword, it will be refreshed to main memory immediately. Other threads that need to use this variable are not fetched from their own private memory. Yes, it's taken directly from main memory. Although volatile keywords ensure that variables are visible to all threads, operations in java code are not atomic operations.

When we look at the bytecode (javap -verbose InheritThreadClass.class) using the javap command, we find that four instructions (getstatic, iconst_1, iadd, putstatic) are used for this incremental operation in the virtual machine. When the getstatic instruction pushes the value of a onto the top of the stack, the volatile keyword guarantees that the value of a is correct at this time, but when the iconst_1 and Iadd instructions are executed, other threads may have increased the value of a, and the value already on the top of the operation stack becomes outdated data, so the smaller value of a may be synchronized back to main memory after the execution of the putstatic instruction. So it's not an atomic operation, so it's not a safe operation in the case of multithreading. In fact, this is not the most rigorous, because even if the compiled bytecode uses only one instruction for operation, it does not mean that the instruction is atomic operation. Because when a bytecode instruction is interpreted and executed, the interpreter needs to run many lines of code to implement the semantics of the instruction, and even if it is compiled and executed, a bytecode instruction may need to be converted into several local machine code instructions.

So the semantic description of volatile variables'visibility to other threads does not lead to the conclusion that volatile variable-based operations are safe under high concurrency.

How can this self-incremental operation be thread-safe under high concurrency? synchronized can be used, but the performance overhead of locking is too high, and high concurrency is not a wise choice. You can use AtomicInteger atomic classes under concurrent package java.util.concurrent.atomic.
Look at the code:

    private static volatile AtomicInteger a = new AtomicInteger(0);
    
    @Override
    public void run() {
        for(int i = 0; i < 1000; i++){
            a.getAndIncrement();
        }
    }

The above code can run correctly in high concurrency, and each output is 100000.
Look at the Atomic Integer source code:

**//Partial key fields**
private static final Unsafe unsafe = Unsafe.getUnsafe();
/*
  valueOffset This refers to the offset of the corresponding field in the class, which is initialized by calling the objectFieldOffset() method in the static block below.
*/
private static final long valueOffset;

static {
  try {
    valueOffset = unsafe.objectFieldOffset
        (AtomicInteger.class.getDeclaredField("value"));
  } catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;

//The objectFieldOffset method is a local method
public native long objectFieldOffset(Field field);

// One of the constructors of Atomic Integer
public AtomicInteger(int initialValue) {
    value = initialValue;
}
//Source code implementation of getAndIncrement() method
public final int getAndIncrement() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return current;
    }
}
//Implementation of get() method
public final int get() {
    return value;
}
/*compareAndSet(int expect, int update)Direct calls inside the method
 *unsafe The compareAndSwapInt method, which directly implements the source code of compareAndSwapInt
 *Compare the values in memory with the expected values at the offset location of obj, and update them if they are the same.
 *This is a local method and should be atomic, so it provides an interruptible way to update.
*/
public native boolean compareAndSwapInt(Object obj, long offset,  
                                            int expect, int update); 
    

Posted by shamuntoha on Sun, 07 Apr 2019 18:27:30 -0700