In order to solve the problems of atomicity, visibility and order in concurrent programming, the Java language provides a series of keywords related to concurrent processing, such as synchronized, volatile, final, concurren package, etc. In the previous article, we also introduced the use and principles of synchronized. In this article, we examine another keyword, volatile.

This article focuses on the use of volatile, the principle of volatile, and how volatile provides visibility and order protection.

The keyword volatile is found in many languages, not just the Java language, and its usage and semantics are different. Especially in C, C++, and Java, the volatile keyword is used. Can be used to declare variables or objects. Here’s a quick look at the Volatile keyword in the Java language.

The use of the volatile

Volatile, often referred to as “lightweight synchronized,” is an important keyword in Java concurrent programming. Unlike synchronized, volatile is a variable modifier and can only be used to modify variables. Cannot decorate methods, code blocks, etc.

The use of volatile is relatively simple, requiring only the modifier volatile when declaring a variable that can be accessed by multiple threads simultaneously.

public class Singleton {

private volatile static Singleton singleton;

private Singleton (){}

public static Singleton getSingleton() {

if (singleton == null) {

synchronized (Singleton.class) {

if (singleton == null) {

singleton = new Singleton();

}

}

}

return singleton;

}

}

The above code, for example, is a typical singleton implemented in the form of double-lock verification, where the volatile keyword is used to modify a singleton that may be accessed simultaneously by multiple threads.

The principle of volatile

Before anyone asks you what the Java memory model is, send this article to them. We have described how to increase processor execution speed by adding multiple levels of cache between processor and memory. However, due to the introduction of multi-level cache, there is the problem of cache data inconsistency.

For volatile variables, however, when a volatile variable is written, the JVM sends an instruction prefixed with lock to the processor to write the cached variable back into system main memory.

However, even if it is written back to memory, if the value cached by other processors is still old, it will be a problem to perform the calculation operation. Therefore, in multi-processors, to ensure that the cache of each processor is consistent, the cache consistency protocol is implemented

Cache consistency protocol: Each processor by sniffing the spread of the data on the bus to check the value of the cache is expired, when the processor found himself cache line corresponding to the memory address has been changed, and will be set for the current processor cache line in invalid state, when the processor wants to modify the data operation, will be forced to read the data from the system memory to the processor cache.

So, if a variable is volatile, its value is forcibly flushed into main memory after each data change. The caches of other processors also load the value of this variable from main memory into their caches because they comply with the cache consistency protocol. This ensures that the value of a volatile is visible in multiple caches in concurrent programming.

Volatile and visibility

Visibility means that when multiple threads access the same variable and one thread changes the value of the variable, other threads can immediately see the changed value.

When someone asks you again what the Java memory model is, we send him this article for analysis: The Java memory model stipulates that all variables are stored in the main memory, and each thread has its own working memory. The working memory of the thread stores a copy of the main memory of the variables used in the thread. All operations on variables must be carried out in the working memory of the thread, instead of reading and writing the main memory directly. Different threads cannot directly access variables in each other’s working memory, and the transfer of variables between threads requires data synchronization between their own working memory and main memory. Therefore, it is possible for thread 1 to change the value of a variable, but thread 2 is not visible.

As described earlier in the principle of volatile, the Volatile keyword in Java provides the ability to synchronize modified variables to main memory immediately after they are modified, and to flush variables from main memory each time they are used. Therefore, volatile can be used to ensure visibility of variables in multithreaded operations.

Volatile and order

Orderliness means that the order in which a program is executed is the order in which the code is executed.

In addition to the introduction of time slices, due to processor optimization and instruction rearrangement, the CPU may also execute input code out of order. For example, load->add->save may be optimized into Load ->save-> Add. This is where there may be an order problem.

In addition to ensuring data visibility, volatile has the power to prevent instruction reordering optimizations, etc.

Ordinary variables only guarantee that the correct result will be obtained wherever the method depends on the assignment, but not in the same order of execution as in the program code.

Volatile prevents instruction reordering, ensuring that code is executed in strict order. This guarantees order. Operations on volatile variables are performed in strict code order: load->add->save: Load, add, save.

Volatile and atomicity

Atomicity means that an operation is uninterruptible and must be performed completely or not at all.

We examined how multithreading is a problem in Concurrent programming in Java: threads are the basic unit of CPU scheduling. The CPU has the concept of time slice and will schedule threads according to different scheduling algorithms. When a thread starts executing after the time slice is acquired, it loses CPU usage after the time slice is exhausted. So in multithreaded scenarios, atomicity problems occur because time slices rotate between threads.

In our previous article on synchronized, we mentioned that the bytecode monitorenter and Monitorexit are required to ensure atomicity, but that volatile has nothing to do with them.

Therefore, volatile does not guarantee atomicity.

Volatile can be used instead of synchronized in two scenarios:

1. The result of the operation does not depend on the current value of the variable, or can ensure that only a single thread will modify the value of the variable.

2. Variables do not need to participate in invariant constraints together with other state variables.

In all scenarios other than the above, you need to use other means to ensure atomicity, such as synchronized or concurrent packages.

Let’s take a look at volatile and atomicity:

public class Test {

public volatile int i = 0;

public void increase() {

i++;

}

public static void main(String[] args) {

final Test test = new Test();

for(int i=0; i<10; i++){

new Thread(){

public void run() {

for(int j=0; j<1000; j++)

test.increase();

};

}.start();

}

While (thread.activecount ()>1) // Ensure that all previous threads have finished executing

Thread.yield();

System.out.println(test.i);

}

}

The above code is relatively simple, just create 10 threads and each perform i++ 1000 times. Normally, the output of the program should be 10000, but the result of multiple executions is less than 10000. This is why volatile cannot satisfy atomicity.

The reason for this is that volatile guarantees the visibility of I across threads. But there’s no guarantee that i++ is atomic.

I ++ operation, there are three steps: load I, add I,save I. In multi-threaded scenarios, if these three steps are not executed in sequence, then there is a problem.

As shown in the figure above, two threads executing i++ at the same time would expect a result of 3 if instructions were allowed to be rearranged, but the actual result could be 2 or even 1.

Summary and Reflection

We’ve covered the volatile keyword and the synchronized keyword. We now know that synchronized guarantees atomicity, order, and visibility. Volatile only ensures order and visibility. By the way, I recommend an architecture exchange group: 617434785, which will share some videos recorded by senior architects: Spring, MyBatis, Netty source code analysis, high concurrency, high performance, distributed, microservice architecture principle, JVM performance optimization, which have become necessary knowledge system for architects. You can also receive free learning resources. I believe that for those who have already worked and met technical bottlenecks, there will be content you need in this group.

So, let’s look at the singleton of a double checklock implementation. Since synchronized is used, why volatile?

public class Singleton {

private volatile static Singleton singleton;

private Singleton (){}

public static Singleton getSingleton() {

if (singleton == null) {

synchronized (Singleton.class) {

if (singleton == null) {

singleton = new Singleton();

}

}

}

return singleton;

}

}