This is the 13th day of my participation in the August More Text Challenge.More challenges in August
This article is from chaoCode, chaoCode, chaoCode, chaoCode and chaoCode.
Violators, the author reserves the right to pursue.
preface
For thread-safe operations on shared data, we use the volatile keyword to modify some shared data to achieve a data visibility. So today we’ll look at the use of the volatile keyword.
Recommended column: Concurrent programming column
The effect of volatile
atomic
One or more operations either succeed or fail. Operations that meet atomicity cannot be interrupted.
visibility
When multiple threads access a shared variable, one thread modifies the variable, and the other threads immediately see the modified value.
order
The order in which the program is executed is the order in which the code is executed. The JMM model allows the compiler and processor to optimize instruction reordering for efficiency. Instruction reordering is sequential in a single thread and unordered in multiple threads. So in multi-threaded concurrent programming, we need to consider how to allow partial instructions to be reordered in multi-threaded environment.)
The synchronized keyword guarantees the above three characteristics.
synchronized
It is a synchronous lock. The code in the synchronous block is equivalent to a single thread execution at the same time, so there is no atomicity and instruction reordering problemsynchronized
The JMM has two provisions for the semantics of keywords that guarantee memory visibility:- Before the thread is unlocked, the latest value of the shared variable must be flushed to main memory.
- Before the thread locks, it clears the values of shared variables in working memory, flushing values from main memory.
The volatile keyword guarantees visibility and order, not atomicity.
So how does volatile guarantee visibility and order? We will start with the implementation basics based on the JMM level, and the following two chapters will introduce the underlying principles.
1.1. Visibility of volatile variables
The Java Virtual Machine specification defines a Java Memory Model (JMM) to mask the differences in Memory access between different hardware and operating systems, so that Java programs can achieve consistent concurrency across all platforms. The main goal of the Java memory model is to define the rules for accessing variables in a program, the details of storing variables in and out of memory in the virtual machine.
Before getting into the JMM, let’s look at the memory model of modern computers.
The memory model of modern computers
In fact, the speed of CPU and memory in early computers was about the same, but in modern computers, the speed of CPU instruction is far faster than the speed of memory access. Because of the difference of several orders of magnitude between the computing speed of the computer’s storage device and that of the processor, So modern computer systems have to add a buffer between memory and the processor, a Cache that reads and writes as fast as possible as the processor can run.
Copying the data needed for an operation to the cache allows the operation to proceed quickly. When the operation is complete, it is synchronized from the cache back to memory so that the processor does not have to wait for slow memory reads and writes.
Cache-based storage interaction is a good solution to the speed contradiction between processor and memory, but it also introduces higher complexity to computer systems because it introduces a new problem: CacheCoherence.
In a multiprocessor system, each processor has its own cache, and they share the same MainMemory.
JMM(Java MemoryModel)
The JavaMemoryModel describes the rules for accessing variables (shared by threads) in a Java program, as well as the low-level details of storing variables in the JVM, storing them into memory, and reading variables from memory.
All shared variables are stored in main memory, by which I mean instance variables and class variables, excluding local variables, which are thread private and therefore do not have contention issues.
Each thread also has its own working memory, which keeps working copies of variables used by the thread.
All operations on variables must be performed by the thread in working memory, rather than directly reading or writing variables in main memory.
Different threads cannot directly access the variables in each other’s working memory, and the values of variables between threads need to be passed through the main memory.
Relationship between local memory and main memory:
It is this mechanism that leads to the visibility problem.
For common shared variables, thread A modifies the variables to be represented in the thread’s working memory. If thread B uses this variable before synchronization to main memory and gets the value before modification from main memory, then the shared variable values are inconsistent, that is, the thread visibility problem occurs.
Volatile definition:
- After a write to a volatile variable, the JMM forces the latest variable values in working memory to be flushed to main memory
- Write operations invalidate caches in other threads
In this way, when other threads use the cache and find that the variable is invalid in the local working memory, they fetch it from the main memory. In this way, the variable obtained is the latest value, which makes the thread visible.
1.2. Disallow instruction reordering on volatile variables
To improve performance, compilers and processors often reorder instructions from a given code execution order.
What are the types of reordering? How will the source code be reordered to final execution?
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.
Volatile prevents instruction reordering by adding “memory barriers” to instruction sequences when the compiler generates bytecode.
Hardware level “memory barrier” :
- Sfence: A Store Barrier inserted after a write instruction, which allows the latest data written to the cache to be written back to main memory, ensuring that the data is immediately visible to other threads
- Lfence: a Load Barrier inserted before reading instructions, which invalidates data in the cache and reloads data from main memory to ensure that the latest data is read.
- Mfence: Modify /mix Barrier, which functions as both sfence and Lfence
- Lock prefix: Lock is not a memory barrier, but a lock. The memory subsystem is locked during execution to ensure sequential execution, even across multiple cpus.
JMM level “memory barrier” :
-
LoadLoad barrier: for statements such as Load1; LoadLoad; Load2: Ensure that the data to be read by Load1 is completed before the data to be read by Load2 and subsequent read operations are accessed.
-
StoreStore barrier: for statements like Store1; StoreStore; Store2. Before Store2 and subsequent write operations are executed, ensure that the write operations of Store1 are visible to other processors.
-
LoadStore barrier: for statements such as Load1; LoadStore; Store2, ensure that the data to be read by Load1 is completed before Store2 and subsequent write operations are flushed out.
-
StoreLoad barrier: for statements like Store1; StoreLoad; Load2, ensure that writes to Store1 are visible to all processors before Load2 and all subsequent reads are executed.
JVM implementations implement memory barriers before and after volatile reads to maintain some degree of order. As follows:
LoadLoadBarrier Volatile The read operation LoadStoreBarrier
StoreStoreBarrier Volatile Write operation StoreLoadBarrier
Thank you for watching, if there is a mistake in the article, welcome to the comment section to communicate. If this article has helped you, please like it at 👍.