Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

A preliminary understanding of volatile

The Java language specification version 3 defines volatile as follows: The Java programming language allows threads to access a shared variable. To ensure that a shared variable can be updated accurately and consistently, threads should ensure that the shared variable is acquired separately through an exclusive lock. The Java language provides volatile, which in some cases is more convenient than locking. If a field is declared volatile, the Java thread memory model ensures that all threads see the variable’s value as consistent.

The following code shows the effect of a variable update with and without volatile.

public class VolatileTest {

    public static void main(String[] args) throws InterruptedException {
        VolatileTest test = new VolatileTest();
        test.start();
        for (;;) {
            if (test.isFlag()) {
                System.out.println("hi");
            }
        }
    }
}
class VolatileTest extends Thread {
    private /*volatile*/ boolean flag = false;

    public boolean isFlag() {
        return flag;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        flag = true;
        System.out.println("flag = " + flag);
    }
}

Copy the code

It is not possible to print hi without volatile, but the thread has changed the value of flag. This is where volatile plays a role in this code.

The first property of volatile is that it guarantees visibility

Volatile ensures visibility of shared variables in multiprocessor environments, so what exactly is visibility? One solution to the memory visibility problem is locking, but using locking is cumbersome because of the overhead of thread context switching. Java provides a weak form of synchronization, known as the volatile keyword. This keyword ensures that updates to one variable are immediately visible to other threads.

When a variable is declared volatile, the thread does not cache the value in registers or elsewhere when writing to the variable. Instead, the value is flushed back to main memory.

When another thread reads the shared variable, it retrieves the latest value from main memory, rather than using the value in the current thread’s working memory.

A good way to understand how volatile guarantees visibility is to think of a single read/write to a volatile variable as synchronizing these individual reads/writes using the same lock.

If you look at the assembly instructions for the above code, you will find that the lock instruction is added when modifying volatile member variables. Lock instruction is a kind of control instruction, in multi-threaded environment, lock assembly instruction can be based on the mechanism of bus lock or cache lock to achieve the effect of visibility.

Property 2 of Volatile: Disallows instruction reordering

Reordering is a process by which compilers and processors reorder instruction sequences to optimize program performance.

The type of reorder

A good memory model actually loosens the rules of the processor and compiler, which means that both software and hardware technologies are fighting for the same goal: to make the execution as efficient as possible without changing the results of the program. The JMM minimizes constraints on the bottom layer so that it can play to its strengths. Therefore, when executing a program, the compiler and processor often reorder instructions to improve performance. General reordering can be divided into the following three types:

Compiler optimized reordering. The compiler can rearrange the execution order of statements without changing the semantics of single-threaded programs.

Instruction – level parallel reordering. Modern processors use instruction-level parallelism to superimpose multiple instructions. If there is no data dependency, the processor can change the execution order of the corresponding machine instructions.

Memory system reordering. Because the processor uses caching and read/write buffers, this makes it appear that load and store operations may be performed out of order. As-if-serial is a serial serial.

No matter how reordered, the results of a single thread cannot be changed. The compiler, runtime, and processor must comply with the AS-IF-Serial semantics.

How does volatile guarantee that reordering will not be performed

The Java compiler inserts memory barrier instructions in place to prohibit reordering of a particular type of handler when generating a sequence of instructions. To implement the memory semantics of volatile, the JMM restricts certain types of compiler and processor reordering. The JMM makes tables for volatile reordering rules for compilers:

But volatile writes insert memory barriers in front and behind, whereas volatile reads insert two barriers behind.

Volatile writes and reads as follows:

Third property of volatile: Atomicity is not guaranteed

The so-called atomicity is: indivisible, that is, when a thread is doing a specific business, the middle can not be added or divided, it needs to be complete, or succeed or fail at the same time. Take a look at this code:

public class VolatileAtomic { public static void main(String[] args) { MyTest myTest= new MyTest(); for(int i = 1; i <= 20; i++) { new Thread(() -> { for (int j = 1; j <= 1000; j++) { myTest.addNum(); } }, String.valueOf(i)).start(); } while(Thread.activeCount()>2){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+"\t number= "+myTest.num); } } class MyTest { public volatile int num = 0; public void addNum() { num++; }}Copy the code

If volatile is added, the final result should be 20000, but the actual output is less than or equal to 20000, indicating that volatile does not guarantee atomicity. Volatile does not guarantee atomicity because num++ is thread-safe in multithreading. After the num++ method is compiled into bytecode, it is executed in the following three steps: 1. Copy the value of I from main memory to the working memory of the CPU.

2. The CPU obtains a value from the working memory and performs the i++ operation to refresh the value to the working memory.

3. Update the value in the working memory to the main memory.

Thread 1 changes num to 1 in its workspace and writes it back to main memory. Main memory informs thread 2 that num=1 due to memory visibility. Select * from thread 2 where num=2; Select * from main memory where num=2; select * from main memory where num=3; Multithreading competition schedule reason, however, thread 1 just to write 1 time suspended, thread 2 1 write into main memory, at this time should notify the other thread, main memory value changed to 1, because of its extremely fast thread operation, also did not inform to other threads, just suspended thread 1 will num = 1 again in the main memory, the value of main memory are covered, Missing write values occur;

This problem can be solved using synchronized or atomic variables. Atomic variables implement atomic operations by calling the CAS method of the Unsafe class, Because the CAS is a kind of system primitives, primitive belong to the category of operating system used, consists of a number of instructions, to complete a process, a feature and primitive execution must be continuous, in the process of execution are not allowed to interrupt, said that the CAS is a atomic instruction, will not result in a so-called data inconsistency problem.

conclusion

Volatile is a lightweight synchronization mechanism provided by the Java virtual machine (JVM). It guarantees visibility, prevents instruction reordering, does not guarantee atomicity, and uses volatile to modify attributes so that compilers do not reorder attributes. Volatile ensures security by enabling visibility and disallowing instruction reordering in singleton double-checks.