Two key issues in concurrency
1. How to communicate between threads
2. How to synchronize between threads
Communication refers to the mechanism between threads to exchange information, in imperative programming, there are two communication mechanisms: Shared memory and message delivery; Java concurrency is shared memory, and communication between threads is always implicit.
Synchronization refers to the mechanism in the program to control the relative order of operations between different threads, in the shared memory concurrency model, synchronization is explicit.
The memory model of Java
1. Shared variables: The elements allocated in the heap memory are shared variables, including instance fields, static fields, and array elements.
2, non-shared variables: the allocation on the stack are non-shared variables, mainly refers to local variables. This variable is thread-private, not shared between threads, and there is no memory visibility issue.
1. The main memory in the diagram is used to store shared variables, and the main memory is common to all threads.
2, local memory is an abstract concept, unlike the main memory is real, each thread has a local memory, used to hold a copy of the shared variables used by the thread.
If thread A and thread B need to communicate, you must go through the following two procedures:
1. Thread A flushes the shared variables that have been modified in memory to the main memory.
2. Thread B into main memory to read the shared variable that thread A has updated
This process is similar to the process characteristics of the 7-layer model of the computer network, which must be passed from top to bottom, then through the underlying physical link, and finally from the bottom to the top to complete a communication.
One, inter-memory interaction operations
Inter-memory interaction mainly refers to the interaction between the working memory (local memory) and the main memory, that is, how a variable is copied from the main memory to the working memory, and how to flush from the working memory to the main memory for some implementation details. The Java memory model defines the following eight actions to complete:
1, Lock: Acting on the main memory, a variable is identified as a thread exclusive state.
2, Unlock: Acting on the main memory, a locked variable is released, after the release of the variable can be locked by other threads.
3. READ: Works on main memory and transfers a variable from main memory to working memory for subsequent load operations.
4. Load: Acts on the working memory and puts the value of the variable that the read operation obtains from main memory into the variable copy of the working memory.
5, use: Acting on the working memory, the value of the variable passed to the execution engine, each time the virtual machine needs to use the variable bytecode instruction will do this operation.
6, assign: Action on the working memory, the value received from the execution engine to the working memory of the variable, whenever the virtual machine encountered the need to assign a value to the variable bytecode instruction to do this operation.
7, Store: Acting on the working memory, the working memory of a variable value to the main memory, so that subsequent write operations.
8. Write: function on main memory, assign store operation value from working memory to variables in main memory
For these 8 operations, there is a principle:
1. Read and Load,store and write operations are not allowed to appear separately.
2. A thread is not allowed to discard its most recent assign operation, where the variable's update in working memory needs to be synchronized to main memory.
3. The thread is not allowed to synchronize data to main memory without any reason (no assign operation has occurred).
4. A new variable can only be generated in main memory and cannot be used directly in the working memory of uninitialized variables.
5, a variable can only be a thread lock at the same time, and lock and unlock need to appear in pairs.
6. If a lock operation is performed on a variable, the value of this variable in the working memory will be emptied, and the load or assgin operation needs to be performed before the execution engine can use the variable.
7. Before executing unclock on a variable, this variable must be synchronized to main memory.
Second, re-order
Reordering is a means by which compilers and processors reorder sequences of instructions in order to optimize program performance. Reordering is divided into 3 categories:
1, compiler optimization reordering: The compiler can reschedule the execution order of the statements without changing the single-line semantics.
2, instruction-level parallel reordering: If there is no data dependency, the processor can change the execution order of the statement corresponding to the machine instruction.
3. Memory system reordering: Because the processor uses caches and buffers, the loading and storage operations may appear to be out of order.
From the Java source code to the final actual execution of the sequence of instructions, the following three kinds of reordering:
Data dependencies:
A = 1; // 1a = 2; // 2
The above shows the write-and-write operation, in which the sequence of 1 and 22 steps is disrupted, and the results of the program execution are changed, and the data is dependent.
double pi = 3.14; // A double r = 1.0; // B double area = pi * R * r; // C
As you can see here, A and C, B, and C have data dependencies, where A and b have no data dependencies, so the compiler and the processor can reorder AB.
The above AB scenario is consistent with as-if-serial semantics: no matter how it is reordered, the execution results of a single-threaded sequence cannot be changed. The compiler and processor must adhere to the as-if-serial semantics when reordering.
Three, memory barrier
A memory barrier, also known as a memory fence, is a CPU instruction that controls reordering and memory visibility issues under specific conditions. Since the operating system is now multiprocessor, each processor has its own cache, and these caches do not interact with memory in real time. This leads to inconsistent data caching on different CPUs, and in multithreaded programs there are some unusual behaviors. The underlying operating system provides a memory barrier to solve these problems. There are currently 4 types of barriers:
1. Loadload Barrier:
For statement Load1; Loadload; Load2, the data to be read by the LOAD1 is guaranteed to be read before Load2 and subsequent read operations are accessed.
2. Storestore Barrier:
For statement Store1; Storestore; Store2, ensure that Store1 writes are visible to other processors before Store2 and subsequent write operations.
3. Loadstore Barrier:
For statement Load1; Loadstore; Store2, the data to be read is guaranteed to be read by the Load1 before Store2 and subsequent write operations.
4. Storeload Barrier:
For statement Store1; Storeload; Load2, ensure that Store1 writes are visible to all processors before Load2 and all subsequent read operations are performed.
There are two common ways in which Java uses memory barriers:
1, using the volatile modifier variable, the write operation of the variable, will insert the Storeload barrier.
2. Using the Synchronized keyword-wrapped code area, when the thread enters the area to read the variable information, it is guaranteed to read the most recent value because the write operation to the variable within the synchronization area flushes the current thread's data into main memory when it leaves the synchronization area. And the data can not be read from the cache, only read from the main memory, to ensure the validity of the data. This is the insertion of the Storestore barrier.
Four, volatile memory semantics and implementation
Keyword volatile is a lightweight synchronization mechanism provided by a Java virtual machine that can only be used to modify variables, guaranteeing the visibility of variables in multithreaded situations, but not guaranteeing the atomicity of variables. Volatitle modified variables have the following two characteristics:
1. Visibility
Ensure that this variable is visible to all threads, where the visibility is that when a thread modifies the value of the variable, the modified value is immediately known to all other threads. The normal variable does not do this because the normal variable needs to synchronize the modified value from the working memory to the main memory before it can be visible to other threads.
Volatile variables can also have inconsistent data in the working memory of each thread, but because the execution engine does not see inconsistencies before each use, it can be considered that there is no inconsistency.
The previous increase in volatile does not guarantee atomicity, see this code below:
Public classvolatiletest{Private Static volatile intRAC = 0; Private Static Final intthreadcnt = 20; Public Static voidIncrease () {RAC++; } Public Static voidMain (string[] args) {thread[] Thread=Newthread[threadcnt]; for(inti=0; i<threadcnt; i++) {Thread[i]=NewThread (NewRunnable () {@Override Public voidrun () { for(intj=0; J < 10000; J + +) {increase (); } }}); Thread[i].start (); } while(Thread.activecount () > 1) {Thread.yield (); } System.out.println (RAC); }}
If this code is correct concurrency, the result should be 200000, but we get the result is smaller than this value, and each time the results are different. (The number of self-increment operations for each thread here is large enough, 10000 is possible, because if it is too small, 20 threads will be executed sequentially without concurrency, getting the correct results and not having the kind of scene we want to make).
We use the following command to get the byte code of the increase method as follows:
As you can see in the self-increment operation Rac++ is made up of four bytecode instructions, here you can know why the exception occurred: the first step of the getstatic instruction to the value of the RAC to the top of the operation Stack, volatile guarantees that the value of the RAC is correct at this time, but in the execution of the second, In three steps, other threads may have increased the value of the RAC, so the value of the RAC at the top of the stack is the expired data, and the RAC value is small when the last call to the putstatic instruction synchronizes the RAC to main memory;
2. Prohibition of order reordering optimization five, Happens-before
JMM has adopted different strategies for reordering two different properties:
1. For reordering of program execution results, JMM requires the compiler and processor to disallow this reordering.
2. JMM does not require the compiler and processor to reorder the results of the program execution (JMM allows this reordering)
Happens-before Rules:
1. Each operation in one thread is happens-before to any subsequent action in that thread.
2, to a lock unlock happens-before with the subsequent locking of this lock.
3, write to a volatile domain Happens-before in any subsequent reading of this volatile domain.
4, if a happens-before B, and B happens-before C, then a happens-before c
5. If thread A performs an operation Threadb.start () (initiates a B thread), then the Threadb.start () operation of the A thread happens-before any action in thread B.
6. If thread A performs an operation Threadb.join () and returns successfully, any action in thread B happens-before the successful return of thread A from the Threadb.join () operation.
Java memory model