JVM Part 15 (conclusion: memory model)

JMM (java memory model)

Java Memory Model means Java Memory Model (JMM). In short, JMM defines a set of rules and guarantees for the visibility, ordering and atomicity of data when reading and writing shared data (member variables and arrays) by multiple threads.

Atomicity

Atomicity: one or more operations are non interruptible, either all executed or all failed.
Atomic operation: atomic operation.
From a simple code, analyze the problem

public class Demo4_1 {
    static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {      
            for (int j = 0; j < 50000; j++) {
                i++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 50000; j++) {
                i--;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

Perform i + + and i – operations on static variable i 5000 times in two threads. Then output the value of i and execute it many times. It is found that the results are different each time.
This is because i + + and i – operations are not atomic operations, and the two threads will interfere with each other during execution, resulting in errors in the results.
Bytecode instruction corresponding to i + +

getstatic i 	// Gets the value of the static variable i
iconst_1 		// Prepare constant 1
iadd 			// Addition, adding 1 and i values
putstatic i 	// Store the modified value into the static variable i

i – corresponding bytecode instruction

getstatic i 	// Gets the value of the static variable i
iconst_1 		// Prepare constant 1
isub 			// subtraction
putstatic i 	// Store the modified value into the static variable i

These instructions may be executed alternately. for example

getstatic i 	// Thread 1 - get the value of static variable i, i=0 in the thread
getstatic i 	// Thread 2 - get the value of static variable i, i=0 in the thread
iconst_1 		// Thread 1 - prepare constant 1
iadd 			// Thread 1 - self incrementing i=1
putstatic i 	// Thread 1 - stores the modified value into static variable I, static variable i=1
iconst_1 		// Thread 2 - prepare constant 1
isub 			// Thread 2 - self decreasing thread i=-1
putstatic i 	// Thread 2 - store the modified value into static variable I, static variable i=-1

The static variables in JMM are stored in the main memory. When the thread executes, the value of i is read into the working memory of the thread. After the modification operation is completed, it is written back to the main memory. The two threads operate on i in the main memory concurrently, resulting in an error.

resolvent
Use synchronized to solve the operation on the same variable when threads are concurrent.
Syntax:

synchronized(object){
	Code to be used as atomic operation
}

Make changes in the code and use the synchronized keyword. Treat i + + and i – as atomic operations.

static Object obj = new Object();
for (int j = 0; j < 50000; j++) {
	synchronized(obj){
		i++;
	}
}
for (int j = 0; j < 50000; j++) {
	synchronized(obj){
		i--;
	}
}

When thread t1 executes synchronized(obj), it is like t1 enters the room, locks the door with its back hand, and executes in the door
i + + code.
At this time, if t2 also runs to synchronized(obj), it finds that the door is locked and can only wait outside the door.
When t1 executes the code in the synchronized {} block, it will unlock the door and come out of obj room. The t2 thread is now running
You can enter obj room, lock the door and execute its i – code

visibility

The operation of a thread on an object can be known by other threads, which is called visibility.

static boolean run = true;
public static void main(String[] args) throws InterruptedException {
	Thread t = new Thread(()->{
		while(run){
			// ....
		}
	});
	t.start();
	Thread.sleep(1000);
	run = false; 	// Modify run to stop the while loop in thread t, but thread t will not stop as expected
}

Cause analysis:

  1. In the initial state, the t thread just started to read the value of run from the main memory to the working memory.
  2. Because the t thread frequently reads the value of run from the main memory, the JIT compiler will cache the value of run into the cache in its own working memory to reduce the access to run in the main memory and improve efficiency.
  3. After one second, the main thread modifies the value of run and synchronizes it to main memory, while t is read from the cache in its own working memory
    Take the value of this variable and the result will always be the old value.
    resolvent
    Modify the variable run with volatile (volatile keyword)
    It can be used to modify member variables and static member variables. It can prevent threads from looking up the value of variables from their own work cache and must obtain its value from main memory. Threads operate volatile variables directly in main memory

volatile enables visibility:
1) The variable is immediately flushed to main memory.
2) Invalidate shared variables of other threads immediately. The implication is that it can be accessed from the host when other threads need it.

volatile only guarantees visibility. It is a lightweight operation with relatively higher performance.
synchronized statement blocks can not only ensure the atomicity of code blocks, but also ensure the visibility of variables in code blocks. But the disadvantage is
synchronized is a heavyweight operation with relatively lower performance.

Order

Strange results

int num = 0;
boolean ready = false;
// Thread 1 executes this method
public void actor1(I_Result r) {
	if(ready) {
		r.r1 = num + num;
	} else {
		r.r1 = 1;
	}
} 
// Thread 2 executes this method
public void actor2(I_Result r) {
	num = 2;
	ready = true;
}

Analyze the possibility of the value of r.r1 after execution.

  1. Case 1: thread 1 executes first. At this time, ready = false, so the result of entering the else branch is 1
  2. Case 2: thread 2 executes num = 2 first, but does not have time to execute ready = true. Thread 1 executes, or enters the else branch. The result is 1
  3. Case 3: thread 2 executes to ready = true, and thread 1 executes. This time, it enters the if branch, and the result is 4 (because num has already been executed)
  4. Case 4: thread 2 executes ready = true, switches to thread 1, enters the if branch, adds to 0, and then switches back to thread 2 to execute num = 2

In case 4, first execute ready = true, and then execute num = 2. This phenomenon is called instruction rearrangement, which is some optimization of the JIT compiler at run time. This phenomenon can only be reproduced through a large number of tests: test with the help of the java Concurrent stress testing tool jcstress.
The test results are as follows: 0 occurred 1652 times. Although it is rare, this phenomenon exists.

0 1,652 ACCEPTABLE_INTERESTING !!!!
1 46,460,657 ACCEPTABLE ok
4 4,571,072 ACCEPTABLE ok

resolvent
volatile modified variables can disable instruction rearrangement.

Ordered understanding
The JVM can adjust the execution order of statements without affecting the correctness.

static int i;
static int j;
// Perform the following assignment operations in a thread
i = ...; // More time-consuming operations
j = ...;

It can be seen that whether to execute i or j first will not affect the final result. Therefore, the above code is really executed
When, it can be

i = ...; // More time-consuming operations
j = ...;

It can also be

j = ...;
i = ...; // More time-consuming operations

This feature is called "instruction rearrangement", but in multithreading, "instruction rearrangement" will affect the correctness. For example, the famous double checked locking mode implements singletons.

public final class Singleton {
	private Singleton() { }
	private static Singleton INSTANCE = null;
	public static Singleton getInstance() {
		// An instance is not created before it enters the internal synchronized code block
		if (INSTANCE == null) {
			synchronized (Singleton.class) {
				// Maybe another thread has created an instance, so judge again
				if (INSTANCE == null) {
					INSTANCE = new Singleton();
				}
			}
		} 
		return INSTANCE;
	}
}

The above implementation features are:
1. Laziness
2. synchronized locking is used only when getInstance() is used for the first time. No locking is required for subsequent use

However, in a multithreaded environment, the above code is problematic. The bytecode corresponding to INSTANCE = new Singleton() is:

0: new #2 				// class cn/itcast/jvm/t4/Singleton
3: dup					
4: invokespecial #3 	// Method "<init>":()V
7: putstatic #4 		// Field

When executing new Singleton, if instruction rearrangement occurs in 4 and 7, execute 7 now and then 4.
Corresponding to two threads t1 and t2, thread t1 executes 7 when creating an object. At the same time, thread t2 judges that the INSTANCE is not empty and obtains the INSTANCE. However, at this time, 4 in t1 has not been executed, that is, the INSTANCE has not executed the construction method, and the INSTANCE obtained by thread t2 is an object that has not been constructed. In this case, an error occurs when using INSTANCE.

Use volatile modification on INSTANCE to disable instruction rearrangement.

happens-before

Happens before specifies which write operations are visible to the read operations of other threads. It is a summary of a set of rules for visibility and ordering,
Regardless of the following happens before rules, JMM does not guarantee that a thread writes to a shared variable and is visible to other threads' reading of the shared variable.

1. The writing of the variable before the thread unlocks m is visible to the reading of the variable by other threads that lock m next
That is, synchronized ensures visibility.
The value of x is modified in t1 and is visible in t2.

static int x;
static Object m = new Object();
new Thread(()->{
	synchronized(m) {
		x = 10;
	}
},"t1").start();
new Thread(()->{
	synchronized(m) {
		System.out.println(x);
	}
},"t2").start();

2. When a thread writes a volatile variable, it is visible to other threads reading the variable

volatile static int x;
new Thread(()->{
	x = 10;
},"t1").start();
new Thread(()->{
	System.out.println(x);
},"t2").start();

3. The writing of the variable before the thread starts is visible to the reading of the variable after the thread starts

static int x;
x = 10;
new Thread(()->{
	System.out.println(x);
},"t2").start();

4. The write to the variable before the end of the thread is visible to the read after other threads know it ends (for example, other threads call t1.isAlive() or
t1.join() wait for it to end)

static int x;
Thread t1 = new Thread(()->{
	x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);

5. Before thread t1 interrupts t2 (interrupt), the write to the variable is visible to other threads after they know that t2 is interrupted (pass through)
(t2.interrupted or t2.isInterrupted)

static int x;
public static void main(String[] args) {
	Thread t2 = new Thread(()->{
		while(true) {
			if(Thread.currentThread().isInterrupted()) {
				System.out.println(x);
				break;
			}
		}
	},"t2");
	t2.start();
	new Thread(()->{
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} 
		x = 10;
		t2.interrupt();
	},"t1").start();
	while(!t2.isInterrupted()) {
		Thread.yield();
	} 
	System.out.println(x);
}

The writing of the default value of the variable (0, false, null) is visible to the reading of the variable by other threads
It has transitivity. If x HB - > y and Y HB - > Z, then there is x HB - > Z

CAS

CAS is Compare and Swap, which embodies the idea of optimistic locking.

CAS underlying principle

An example: for example, multiple threads need to perform a + 1 operation on a shared integer variable

// You need to keep trying
while(true) {
	int Old value = Shared variable ; // For example, you get the current value 0
	int result = Old value + 1; // Add 1 to the old value of 0, and the correct result is 1
	/*
	At this time, if another thread changes the shared variable to 5, the correct result 1 of this thread will be invalidated
	compareAndSwap Return false and try again until: true
	compareAndSwap Returns true, which means that other threads do not interfere when I modify this thread
	*/
	if( compareAndSwap ( Old value, result )) {
		// Successful, exit loop
	}
}

When obtaining a shared variable, in order to ensure the visibility of the variable, you need to use volatile decoration. The combination of CAS and volatile can realize lock free concurrency, which is suitable for the scenario of non fierce competition and multi-core CPU.

  1. Because synchronized is not used, the thread will not be blocked, which is one of the factors to improve efficiency
  2. If the competition is fierce, it can be expected that retry will occur frequently, but the efficiency will be affected

The CAS bottom layer relies on an Unsafe class to directly call CAS instructions at the bottom of the operating system. The following is an example of thread safety protection directly using Unsafe objects

class DataContainer {
	private volatile int data;
	static final Unsafe unsafe;
	static final long DATA_OFFSET;
	static {
		try {
			// Unsafe objects cannot be called directly. They can only be obtained through reflection
			Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
			theUnsafe.setAccessible(true);
			// Get unsafe object
			unsafe = (Unsafe) theUnsafe.get(null);
		} catch (NoSuchFieldException | IllegalAccessException e) {
			throw new Error(e);
		} try {
			// The offset of the data property in the DataContainer object, which is used by Unsafe to access the property directly
			DATA_OFFSET = unsafe.objectFieldOffset(DataContainer.class.getDeclaredField("data"));
		} catch (NoSuchFieldException e) {
			throw new Error(e);
		}
	} 
	// Use unsafe add operation
	public void increase() {
		int oldValue;
		while(true) {
			// Get the old value of shared variable. You can add breakpoints in this line and modify data debugging to deepen understanding
			oldValue = data;
			// cas tries to change the data to the old value + 1. If the old value is changed by another thread during the period, false is returned
			if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue + 1)) {
				return;
			}
		}
	} 
	// Use unsafe subtraction operation
	public void decrease() {
		int oldValue;
		while(true) {
			oldValue = data;
			if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - 1)) {
				return;
			}
		}
	}
	public int getData() {
		return data;
	}
}

Call class

import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class TestCAS {
	public static void main(String[] args) throws InterruptedException {
		DataContainer dc = new DataContainer();
		int count = 5;Thread t1 = new Thread(() -> {
			for (int i = 0; i < count; i++) {
				dc.increase();
			}	
		});
		t1.start();
		t1.join();
		System.out.println(dc.getData());
	}
}

Optimistic lock and pessimistic lock:

CAS is based on the idea of optimistic locking: the most optimistic estimation is that it is not afraid of other threads to modify shared variables. Even if it is changed, it doesn't matter,
I'll try again.
synchronized is based on the idea of pessimistic locking: the most pessimistic estimation is to prevent other threads from modifying shared variables. I locked it
You don't want to change it. Only after I change it can you have a chance.

Atomic operation class

juc (java.util.concurrent) provides atomic operation classes, which can provide thread safe operations, such as AtomicInteger
AtomicBoolean, etc. their bottom layer is implemented by CAS technology + volatile.
You can use AtomicInteger to rewrite the previous example
Using the AtomicInteger class, you can ensure thread safety.

// Create atomic integer object
private static AtomicInteger i = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(() -> {
		for (int j = 0; j < 5000; j++) {
			i.getAndIncrement(); // Get and auto increment i++
			// i.incrementAndGet(); //  Auto increment and get + + I
		}
	});
	Thread t2 = new Thread(() -> {
		for (int j = 0; j < 5000; j++) {
			i.getAndDecrement(); // Get and subtract i--
		}
	});
	t1.start();
	t2.start();
	t1.join();
	t2.join();
	System.out.println(i);
}

synchronized optimization

In the Java HotSpot virtual machine, each object has an object header (including class pointer and Mark Word). Mark Word usually saves
Store the hash code and generation age of this object. When locking, these information will be replaced with tag bit, thread lock record pointer, heavyweight lock pointer, thread ID, etc.

Lightweight Locking

If an object has multithreaded access, but the multithreaded access time is staggered (that is, there is no competition), lightweight locks can be used to optimize.
Lightweight locks are not intended to replace heavyweight locks. Their original intention is to reduce the performance consumption of traditional heavyweight locks using operating system Mutex without multi-threaded competition.

Locking process:
The stack frame of each thread will contain a lock record structure, which can store the Mark Word of the locked object.

Lightweight locking process

  1. First, the JVM creates a space for storing lock records in the current thread stack frame;
  2. Copy the Mark Word in the object header to the lock record, which is called Displaced Mark Word;
  3. The two digits in the object Mark Word record whether the object is locked (01 no lock, 00 lightweight lock, 10 lightweight lock). If the object is found to be unlocked, the thread attempts to use CAS to replace the Mark Word in the object header with a pointer to the lock record. Success means that the lock is obtained, failure means that other threads compete for the lock, and the current thread attempts to use spin operation to obtain the lock.
  4. The lock is obtained successfully. The updated lock is marked as 00, indicating that the object has a lightweight lock.

Unlocking process:

  1. Write the mark word stored in the lock record back to the locked object.
  2. The lock mark of the object is changed to 01, indicating no lock

Lightweight lock expansion:
When CAS lightweight lock is added, the lock fails, indicating that there is competition. Lock expansion will change the lightweight lock into a heavyweight lock.
If the thread t fails to lock an object, it will expand the lock, use CAS to change the Mark to the weight lock (the lock Mark is changed to 10 to indicate the weight lock), and then set the weight lock pointer in the object header to block the thread. When waiting for the thread to unlock the locked object, wake up the blocking thread according to the weight lock pointer.

Heavyweight lock spin

When competing for heavyweight locks, you can also use spin to optimize. If the current thread spins successfully (that is, the lock holding thread has exited the synchronization block and released the lock), the current thread can avoid blocking.
After Java 6, the spin lock is adaptive. For example, if the object has just succeeded in a spin operation, it is considered that the possibility of successful spin this time will be high, so spin more times; Conversely, less spin or even no spin, more intelligent.

Spin: after the attempt to lock fails, it will not block immediately, but try to lock more times. (avoid blocking if the thread holding the lock completes unlocking soon)
Optional failure: multiple retries, no lock, spin failure, blocking.

  1. Spin will occupy CPU time. Single core CPU spin is a waste, and multi-core CPU spin can give play to its advantages.
  2. For example, whether the car stalls when waiting for a red light. Not stalling is equivalent to spinning (the waiting time is short and cost-effective), and stalling is equivalent to blocking (waiting)
    Stay for a long time (cost-effective)
  3. After Java 7, you can't control whether to turn on the spin function

Bias lock

The lightweight lock still needs to perform CAS operation every time it re enters when there is no competition (just its own thread). Java 6 introduces bias lock for further optimization: only when CAS is used for the first time to set the thread ID to the Mark Word header of the object, and then it is found that the thread ID is its own, it means that there is no competition and there is no need to re CAS.

  1. Revoking bias requires upgrading the locked thread to a lightweight lock, during which all threads need to be suspended (STW)
  2. The hashCode of the access object will also revoke the bias lock
  3. If the object is accessed by multiple threads but there is no competition, the object biased to thread T1 still has the opportunity to re bias to T2,
    Reorientation resets the Thread ID of the object
  4. Undo bias and redo bias are performed in batch, with class as the unit
  5. If the undo bias reaches a certain threshold, all objects of the entire class become unbiased
  6. You can actively use - XX:-UseBiasedLocking to disable bias locking

Other optimization

1. Reduce the locking time and keep the synchronization code block as short as possible
2. Reduce the granularity of locks and split a lock into multiple locks to improve concurrency, for example:
2.1 ConcurrentHashMap
2.2 LongAdder is divided into base and cells. When there is no concurrent contention or the cell array is initializing, CAS will be used to accumulate the value to the base. If there is concurrent contention, the cell array will be initialized. The number of cells in the array will be allowed to be modified in parallel. Finally, each cell in the array will be accumulated and the base will be the final value
2.3 LinkedBlockingQueue uses different locks for entering and leaving the queue. Compared with LinkedBlockingArray, there is only one lock, which is more efficient
3. Lock coarsening
Multiple loops entering the synchronization block are not as good as multiple loops in the synchronization block. In addition, the JVM may make the following optimization to coarsen the locking operation of multiple append into one (because they lock the same object, it is not necessary to re-enter multiple times)

new StringBuffer().append("a").append("b").append("c");

4. Lock elimination
The JVM will perform code escape analysis. For example, a locked object is a local variable in a method and will not be accessed by other threads. At this time, all synchronization operations will be ignored by the immediate compiler.
5. Read write separation
CopyOnWriteArrayList
ConyOnWriteSet

JVM completion

Tags: Java jvm

Posted on Sat, 20 Nov 2021 17:52:14 -0500 by will35010