Visibility problems can occur when multiple threads accessing shared variables are running on multi-core cpus. The synchronized keyword and lock solve this problem, but block threads and degrade performance, so Java gives the more lightweight volatile keyword, which does not block threads. Volatile serves two purposes: to ensure visibility of shared variables and to prevent instruction reordering.
To understand what the volatile keyword does and how it works, you need to understand some computer basics.
Three features of concurrent programming
In concurrent programming, thread safety involves three characteristics: atomicity, visibility, and orderliness.
atomic
All or nothing. Atomic operations in Java include:
- In addition to
long
anddouble
An assignment operation of a base type - All references
reference
The assignment operation of java.concurrent.Atomic.*
All operations for all classes in a package
Long and double, because they are updated in two parts on 32-bit operating systems, are not atomic operations. The autoadd operation (I ++) is not an atomic operation because it reads the value of I and assigns it to the local variable TMP, TMP +1, and assigns the result to I, a collection of three atomic operations.
visibility
All threads can see the latest state of shared memory.
When multiple threads access the same variable, the variable can be seen by other threads after being modified by one thread. That is, when multiple threads access the same variable, they see the same value.
order
To improve CPU pipelining parallelism, the compiler reorders instructions that are not dependent.
Java Memory Model (JMM)
Programs are stored in external memory, and code is called into main memory when the process is executed. However, the speed of main memory is much slower than that of the CPU. To improve the performance, a cache is added to the CPU. Instead of directly accessing main memory, the CPU interacts with main memory through the cache. When an instruction is executed, the block where the instruction resides is fetched from main memory to the cache, and then the CPU accesses the cache to obtain the instruction. Cpus typically have multiple cores, each with its own cache. When multiple threads access a shared variable at the same time, they may be running on different kernels. The shared variable has a backup in the cache of each memory. Each thread operates on the backup of the shared variable in the kernel cache, causing cache consistency problems. The Java Virtual Machine specification defines a Java Memory Model (JMM) to mask differences in memory access across hardware and operating systems so that Java programs can achieve consistent memory access across platforms. To achieve better execution performance, the Java memory model does not restrict the execution engine from using the processor’s registers or telling the cache to speed up instruction execution, nor does it restrict the compiler from reordering instructions. Simply put, in the Java memory model, there are problems with cache consistency and instruction reordering.
Volatile role
Volatile is used to guarantee visibility and order of modified variables, but not atomicity.
Shared variable visibility and implementation principles
When a shared variable is volatile, it ensures that the changed value is immediately updated to main memory, and that the new value is re-read from memory when another thread needs to read it. And common Shared variables cannot ensure visibility, because common Shared variables were modified, when will be written into main storage is uncertain, when another thread to read, on the one hand, the memory may have been the original old value, so there is no guarantee that the visibility, on the other hand may access the thread own kernel cache data in the cache, Not main memory data, even if the main memory data has been updated, can not read the latest value.
How visibility is implemented
- Thread 1 modification
volatile
Is immediately written to main memory and invalidates the current block from main memory in other caches. - Other threads in the cache probe bus, when the main memory corresponding to their own block has been modified, the corresponding block in the local cache will be invalidated. When the processor attempts to read or write data and finds that the block is invalid, it transfers the block from the main memory to the cache and reads the data again. In this case, the read data is the latest.
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.
Visibility is also guaranteed with synchronized and Lock, which ensure that only one thread at a time acquies the Lock and executes the synchronization code, flushing changes to variables into main memory before releasing the Lock. However, these two methods block the execution of other threads and have a high performance cost.
Reorder prevention and principle
In the actual execution process, the code is not always executed in the order written. Under the condition that the single-thread execution result is guaranteed, the compiler or CPU may reorder the instructions to improve the execution efficiency of the program. However, in the case of multithreading, instruction reordering can cause some problems, the most common is the double check lock singleton pattern: when implementing the singleton pattern with lazy load mode, it usually uses the double check lock mode (DCL), the code implementation is as follows.
public class Singleton {
public static volatile Singleton singleton;
private Singleton(a) {};
public static Singleton getInstance(a) {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = newSingleton(); }}}returnsingleton; }}Copy the code
The volatile keyword is not typically used in implementations, so why the volatile modifier? To understand this, we need to understand the object creation process. Instantiating an object consists of three steps:
- Allocating memory space
- Initialize an object
- Assigns the address of the memory space to the reference
But since the compiler can reorder instructions, the above procedure could also become the following:
- Allocating memory space
- Assigns the address of the memory space to the reference
- Initialize an object
If this is the case, a multithreaded environment exposes a reference to an uninitialized object, leading to unexpected results. Therefore, to prevent reordering of the process, change the variable to volatile.
Preventing instruction reordering is implemented through memory barriers. When a bytecode file is generated, the compiler inserts a memory barrier into the sequence of instructions to prevent a particular type of handler from reordering. There are three types of memory barriers:
- The x86 “sfence” directive forces all Store instructions that come before the Store Barrier directive to be executed before the Store Barrier directive is executed.
- A Load Barrier is an x86 “ifence” directive that forces all loads following the Load Barrier to be executed after the Load Barrier is executed
- Full Barrier: Full Barrier, which is the “mfence” instruction on x86, combining the functions of load and save barriers.
In the Java memory model, volatile variables insert a store barrier after writes and a load barrier before reads, and the read and write instructions of volatile variables cannot be reordered with any instructions before or after them; they may be reordered. A final field of a class is inserted with a store barrier after initialization to ensure that the final field is visible when the constructor is initialized and ready for use. It is also the JMM that inserts memory barriers before and after volatile variables are read and written to ensure that they are executed sequentially.
Atomicity is not guaranteed
When volatile modifies variables of simple types, such as int, float, and Boolean, the operation on them becomes atomic. With some limitations, volatile does not work if the simple variable volatile modiates is related to its previous value, so the test() method below is not an atomic operation. To make this atomic, use the synchronized keyword, as in test2().
class Test {
volatile int n;
private void test(a) {
n++;
n = n + 1;
}
private synchronized void test2(a) {
n++;
n = n + 1; }}Copy the code
Use volatile with caution. It is not necessary to use volatile on simple variables. All operations on this variable are atomic, and volatile is invalid when the value of the variable is determined by its previous value, as n=n+1 or n++. An operation on a variable is atomic only if its value is independent of a value above it, such as n = m + 1, which is atomic. . In addition, if you use AtomicInteger set (AtomicInteger. The get () + 1), has the concurrency issues like the above situation, in order to use AtomicInteger. GetAndIncrement () to avoid concurrency issues.
conclusion
- Correct usage scenario: write multiple reads, only updated by one thread, all other threads read.
volatile
Is a lightweight synchronization mechanism. During a visit tovolatile
A variable does not lock and therefore does not block the thread of executionsynchronized
Keyword more lightweight synchronization mechanism.volatile
Only memory visibility is guaranteed, not atomicity, so it cannot be replacedsynchronized
And locking mechanism. The latter two mechanisms ensure both visibility and atomicity.volatile
You cannot decorate a variable whose write operation depends on the current value. A simple variable declared as volatile if the current value is related to the variable’s previous valuevolatile
The keyword does not work, which means that none of the following expressions are atomic operations:count++
,count = count+1
.- When the variable to be accessed is already in
synchronized
Not necessary in code blocks, or constantsvolatile
; volatile
The frequent reads and writes from memory and the shielding of necessary code optimizations in the JVM are inefficient compared to normal variables, so use this keyword only when necessary.
reference
The role of volatile in Java The role of volatile and the correct usage of volatile in Java