Introduction to the

The volatile keyword, which makes a variable visible across threads. Thread A and thread B both use A variable. The Java default is that thread A keeps A copy of the variable, so if thread B changes the variable, thread A may not know about it. Using the volatile keyword causes all threads to read the changed value of the variable.

  • Visibility: Writes to volatile fields are guaranteed to be visible to all threads
  • Order (disallows instruction reordering) : The use of the volatile keyword to ensure a degree of order
volatile boolean running = true; void m() { System.out.println("m start"); while(running) { } System.out.println("m end!" ); } public static void main(String[] args) { T01_HelloVolatile t = new T01_HelloVolatile(); new Thread(t::m, "t1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } t.running = false; } // The main thread changes the running variable, which can be read by the child thread. Otherwise, the program will never stopCopy the code

visibility

The memory model of modern computers

Now we common multi-core CPU, quad-core 8G, of course, based on the von Neumann system design of the computer is also flawed! The disadvantage is that the CPU’s processing speed is much faster than the reading and writing speed of the memory. Therefore, the CPU spends most of its time waiting for data to be read from the memory and then writing the data back to the memory.

Modern computer systems buffer data by adding a layer of caches in front of the CPU and main memory, with read and write speeds as close as possible to the CPU’s running speed. In this way, the caches get data from main memory in advance, and the CPU no longer accesses data from main memory, but from cache. This relieves CPU starvation due to slow main memory. (L1, L2, L3)

Cache-based storage interaction resolves the CPU-memory speed conflict nicely, but it also introduces a new problem: Cache Coherence.In a multi-CPU system, each CPU has its own cache, and they share the same main memory. Variable inconsistencies can occur when multiple CPU operations involve the same variable. To address the problem of consistency, it is necessary that each processor follow some protocol when accessing the cache, read and write according to protocols, such as MSI, Illinois Protocol (MESI), MOSI, Synapse, Firefly and Dragon Protocol.

JMM Java memory model

The JMM defines how the Java Virtual Machine (JVM) works in computer memory (RAM), and the JMM is affiliated to the JVM. From an abstract point of view, JMM defines an abstract relationship between threads and Main Memory: Shared variables between threads are stored in Main Memory, and each thread has a private Local Memory where it stores copies of shared variables to read/write. The JVM’s implementation of the Java memory model is the thread stack area and heap area

Multithreaded competition

If now in the main memory has X = 0, A and B at the same time to read the thread X, read their thread in memory, and then do A X++ operation, at this time, the thread A local memory modify complete yourself first, then B and thread communication, thread A X in local memory refresh yourself into main memory, and finally, thread B read B in main memory.

order

When executing a program, the compiler and processor often reorder instructions to improve performance.

Reorder type

  • Compiler optimized reordering. The execution order of statements can be rearranged without changing the semantics of a single-threaded program.
  • Instruction – level parallel reordering. The instruction level parallelism technique is used to overlap multiple instructions. If there are no data dependencies, the processor can change the order in which machine instructions are executed.
  • Memory system reordering. Because the processor uses caching and read/write buffers, this makes the load and store operations appear to be out of order.

reorder

  • In these three cases, the execution result of the program will be changed by reordering the execution order of the two operations.

as-if-serial

No matter how to reorder, the code must be guaranteed to run correctly in a single thread, even can not be correct in a single thread, let alone multi-threaded concurrency, so the concept of as-if-serial was proposed. The as-if-serial semantics mean that all actions can be reordered for optimization, but their reordered result must be the same As the result of the program code itself.

Memory barrier – Disallows reordering

The implementation of the volatile keyword is largely controlled through memory barriers. When the bytecode is generated, the compiler inserts a memory barrier into the sequence of instructions to prevent certain reorders. It is not possible for the compiler to make its own judgment to minimize the total number of insert barriers. To this end, JMM takes a conservative approach:

  • Add StoreStore before each volatile write
  • Add StoreLoad after each volatile write
  • Add LoadLoad after each volatile read
  • Add LoadStore after each volatile read

MESI (famous Cache Consistency protocol)

M: Modified E:Exclusive S:shared I:invalid

  • When the CPU writes data, if the variable is a shared variable, that is, if a copy of the variable exists in other cpus, it sends a signal to inform other cpus to set the cache line of the variable to invalid state.
  • When the CPU reads a shared variable and finds that its cache line for that variable is invalid, it re-reads it from memory.

Happens-before principle

  • If one action happens-before the other, the execution result of the first action will be visible to the second action, and the execution order of the first action precedes the second action.
  • The existence of a happens-before relationship between two operations does not mean that they must be executed in the order specified by the happens-before principle. The reorder is not illegal if the result of the reorder is the same as the result of the happens-before relationship.

pit

Volatile reference types (including arrays) only guarantee the visibility of the reference itself, not the visibility of the internal fields

boolean running = true; volatile static T02_VolatileReference1 T = new T02_VolatileReference1(); void m() { System.out.println("m start"); while(running) { } System.out.println("m end!" ); } public static void main(String[] args) { new Thread(T::m, "t1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } T.running = false; }Copy the code

T.running is modified in the main thread, but the code does not stop.

Volatile does not guarantee inconsistencies when running variables are modified jointly by multiple threads, which means that volatile is no substitute for synchronized

volatile int count = 0; /*synchronized*/ void m() { for(int i=0; i<10000; i++) { count++; } } public static void main(String[] args) { T04_VolatileNotSync t = new T04_VolatileNotSync(); List<Thread> threads = new ArrayList<Thread>(); for(int i=0; i<10; i++) { threads.add(new Thread(t::m, "thread-"+i)); } threads.forEach((o)->o.start()); threads.forEach((o)->{ try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); }}); System.out.println(t.count); }Copy the code
  • In the previous program, it was possible to use synchronized, which guarantees visibility and atomicity; volatile only guarantees visibility
  • Whether volatile is added depends on whether sync is an atomic operation

DCL singleton pattern

private volatile static Instance ins = null; / * * * DCL way for singleton * @ return * / public static Instance getInstance () {if (ins = = null) {synchronized (Instance. Class) {if (ins == null){ ins = new Instance(); } } } return ins; }Copy the code

Assuming that as soon as the thread executes the instance = new Singleton() sentence, which looks like a single sentence but is actually compiled and executed by the JVM, the corresponding change code finds that this sentence is compiled into eight assembly instructions that do roughly three things: 1) Allocate memory for instance instance 2) initialize instance constructor 3) point instance object to allocated memory. Thread two goes directly to the return instance statement, takes the instance and uses it, and then naturally reports an error (the object has not been initialized). Synchronized ensures atomicity of threads, but the instruction formed after compilation of a single statement is not an atomic operation. The solution is to disable instruction reordering optimization, using volatile variables

Another singleton pattern

Internal class writing, which guarantees singletons from the JVM virtual machine and is lazily loaded.

private Singleton() {}	
private static class Inner {
        private static Singleton s = new Singleton();
}
public static Singleton getSingle() {
        return Inner.s;
}
Copy the code

The JMM supplement

JMM, Java Memory Model, is a very important concept in Java, and is the core and foundation of Java concurrent programming. The JMM is a set of protocols defined by Java. It is used to mask differences in memory access between different hardware and operating systems, so that Java programs can run consistently on different platforms.

The Java memory model defines the following eight operations (each of which is atomic) to accomplish the specific protocol of interaction between main memory and working memory, namely the implementation details of how a variable is copied from main memory to working memory and synchronized from working memory to main memory:

  • Lock: A variable that acts on main memory and identifies a variable as a thread-exclusive state.
  • Unlock: Applies to a main memory variable. It unlocks a locked variable before it can be locked by another thread.
  • Read: Acts on a main memory variable, transferring the value of a variable from main memory to the thread’s working memory for subsequent load action
  • Load: Variable acting on working memory, which puts the value of the variable obtained from main memory by the read operation into a copy of the variable in working memory.
  • Use: variable applied to working memory, passing the value of a variable in working memory to the execution engine. This operation is performed whenever the virtual machine reaches a bytecode instruction that requires the value of the variable to be used.
  • Assign: a working memory variable that assigns a value received from the execution engine to the working memory variable. This operation is performed whenever the virtual machine accesses a bytecode instruction that assigns a value to the variable.
  • Store: A variable applied to working memory that transfers the value of a variable in working memory to main memory for subsequent write operations.
  • Write: a variable operating on main memory that transfers store operations from the value of a variable in working memory to a variable in main memory.

The Java memory model also specifies that the following rules must be met when performing the eight basic operations described above:

  • To copy a variable from main memory to working memory, read and load operations are performed sequentially. To synchronize variables from working memory back to main memory, store and write operations are performed sequentially. But the Java memory model only requires that the above operations be performed sequentially, not sequentially, meaning that the operations are not atomic and a group of operations can be interrupted.
  • One of the read and load, store and write operations is not allowed to occur separately, but must occur in pairs.
  • A thread is not allowed to discard its most recent assign operation, i.e. variables that have changed in working memory must be synchronized to main memory.
  • A thread is not allowed to synchronize data from working memory back to main memory without a cause (without any assign operation).
  • A new variable can only be created in main memory. It is not allowed to directly use an uninitialized (load or assign) variable in working memory. Assign and load operations must be performed before use and store operations can be performed on a variable.
  • A variable can be locked by only one thread at a time. However, the lock operation can be repeated by the same thread several times. After the lock operation is performed several times, the variable can be unlocked only after the same number of UNLOCK operations are performed. Lock and unlock must come in pairs
  • If you lock a variable, the value of the variable will be emptied from working memory, and the load or assign operation will be performed again to initialize the variable before the execution engine can use it
  • Unlock cannot be performed on a variable that has not been previously locked by a lock operation. It is also not allowed to unlock a variable that has been locked by another thread.
  • Before an unlock operation can be performed on a variable, the variable must be synchronized to main memory (store and write operations).