preface

Learning record

  • Time: Week 1
  • SMART sub-target: Java multithreading

Learning Java multithreading, to understand the concurrent phenomenon of multithreading may occur, to understand the knowledge of Java memory model is essential.

Record important knowledge points learned.

Note: The Java memory model is related to concurrent programming, not the JVM memory structure (heap, method stack, etc.). The two are not the same thing.

Java memory model

The Java Memory Model (JMM) is a mechanism and specification that conforms to the specification of the Memory Model, shielding the access differences of various hardware and operating systems, and ensuring that the Java program can get the same effect on the access of Memory on various platforms. The goal is to solve atomicity, visibility (cache consistency), and orderliness problems due to multiple threads communicating through shared memory.

Main memory vs. working memory

Let’s start with the cache access operation of the computer hardware:

Registers on the processor can read and write several orders of magnitude faster than memory, and to resolve this speed contradiction, a cache is inserted between them.

The addition of caching brings a new problem: cache consistency. If multiple caches share the same main memory area, data from multiple caches may be inconsistent, and some protocol is required to resolve this problem.

Java’s memory access operations are highly comparable to the hardware caches described above:

In the Java memory model, all variables are stored in the main memory, and each thread has its own working memory. The working memory is stored in the cache or register, which holds the main memory copy of the variables used by the thread. Threads can only manipulate variables in working memory directly, and variable values between threads need to be passed through main memory.

Intermemory operation

The Java memory model defines eight operations to interact with main and working memory

  • Read: Transfers the value of a variable from main memory to the thread’s working memory
  • Load: Executes after read, putting the value of read into a copy of the variable in the thread’s working memory
  • Use: Passes the value of a variable in the thread’s working memory to the execution engine
  • Assign: A variable that assigns a value received from the execution engine to working memory
  • Store: Transfers the value of a variable in working memory to main memory
  • Write: Executes after store, putting the value of store into a variable in main memory
  • Lock: Variable applied to main memory, marking a variable as a thread-exclusive state
  • Unlock: A variable that operates on main memory. It releases a variable that is locked before it can be locked by another thread.

There are three main features of the memory model

atomic

The Java memory model guarantees atomicity for read, load, use, assign, Store, write, Lock, and unlock. For example, an assignment to a variable of type int is atomic. But the Java memory model allows the virtual machine to divide reads and writes to 64-bit data (long, double) that are not volatile into two 32-bit operations. That is, reads and writes to basic data types are atomic, except for long and double, which are non-atomic. ** that is, the load, store, read, and write operations may not be atomic. The ** books warn us that this is all we need to know, because this is an almost impossible exception.

Although the above statement states that access to basic data types is atomic, it does not mean that there are no thread-safety issues in multithreaded environments, such as variables of type int. For detailed examples, please refer to *** example 1 ***.

To ensure atomicity, try the following methods:

  • Use Atomic classes (such as AtomicInteger) for variables of base type
  • In other cases, synchronized mutex can be used to guarantee atomicity of operations within bounded critical regions. The memory interactions are lock and unlock, and the bytecode instructions are Monitorenter and Monitorexit.

visibility

Visibility means that when one thread changes a value in a shared variable, other threads are immediately aware of the change. The Java memory model implements visibility by synchronizing the new value back to main memory after a variable is modified and flushing the value from main memory before the variable is read.

The visibility error problem paradigm is more difficult to model, and those interested can use this article to better understand it.

There are three main ways to ensure visibility:

  • volatile
    • Java memory is divided into main memory and thread working memory. Volatile ensures that changes are immediately synchronized from the current thread working memory to main memory, but other threads still need to access the main memory to keep the threads synchronized.
  • synchronized
    • When the thread acquires the lock, it retrieves the latest value of the shared variable from main memory. When the lock is released, the shared variable is synchronized to main memory. At most one thread can hold the lock.
  • final
    • Fields decorated with the final keyword can be seen by other threads in the constructor once the initialization is complete and no this escape has occurred (other threads access the half-initialized object through this reference).

The use of volatile for the CNT variable in Example 1 does not address the thread-unsafe problem because volatile does not guarantee atomicity of operations.

order

Orderliness means that all operations are in order when observed in the thread. Observing from one thread to another, all operations are out of order because instruction reordering has occurred. In the Java memory model, the compiler and processor are allowed to reorder instructions. The reordering process does not affect the execution of single-threaded programs, but affects the correctness of multi-threaded concurrent execution.

To ensure visibility, the following are the main implementations:

  • volatile
    • The real meaning of volatile is to create memory barriers that prohibit instruction reordering. Reordering does not place subsequent instructions in front of the memory barrier.
  • synchronized
    • It ensures that only one thread executes the synchronized code at any one time, effectively ordering the threads to execute the synchronized code sequentially.

The more difficult and deeper part of ordering is actually instruction reordering. I’m going to quote what I think is a very clear article. Reordering of memory models

Principle of antecedent

Under the JVM memory model, the principle of preempt allows one operation to complete before another without any synchronizer assistance. If the relationship between two operations is not in this column and cannot be deduced from the following rules, they are not guaranteed order and the virtual machine can reorder them at will.

  • Single Thread Rule – Single Thread Rule
    • Also called Program Order Rule in other books – Program Order Rule
    • In a thread, the actions that precede the thread take place before the actions that follow. (To be precise, control flow order, not code order, since there may be logical decision branches)
  • Pipe Lock Rule – Monitor Lock Rule
    • An UNLOCK operation occurs first after a lock operation on the same lock.
  • Volatile Variable rules – Volatile Variable rules
    • A write to a volatile variable occurs first after a read to that variable
  • Thread Start Rule – Thread Start Rule
    • The start() method of the Thread object calls each action of the Thread first.
  • Thread Join Rule – Thread Join Rule
    • The end of the Thread object occurs first when the join() method returns.
  • Thread Interruption Rule – Thread Interruption Rule
    • A call to the interrupt() method occurs when code in the interrupted thread detects the occurrence of an interrupt, which can be detected through the interrupted() method.
  • Object Termination Rule – Finalizer Rule
    • The completion of an object’s initialization (the end of constructor execution) occurs first at the beginning of its Finalize () method.
  • Transitivity – Transitivity
    • If operation A precedes operation B and operation B precedes operation C, then operation A precedes operation C.

In the case of multi-threading, there is basically no relationship between the time order and the antecedent principle. We should not be spared the time order when we measure the concurrency safety problem. Everything must be based on the antecedent principle.

Insert cases to help understand

Case a

code

/** * Three features of the memory model - atomicity verification comparison **@author Richard_yyf
 * @version1.0 2019/7/2 * /
public class AtomicExample {

    private static AtomicInteger atomicCount = new AtomicInteger();

    private static int count = 0;

    private static void add(a) {
        atomicCount.incrementAndGet();
        count++;
    }

    public static void main(String[] args) {
        final int threadSize = 1000;
        final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < threadSize; i++) {
            executor.execute(() -> {
                add();
                countDownLatch.countDown();
            });
        }
        System.out.println("atomicCount: " + atomicCount);
        System.out.println("count: "+ count); ThreadPoolUtil.tryReleasePool(executor); }}Copy the code

Outout

atomicCount: 1000
count: 997
Copy the code

Analysis of the

You can use the following figure to help understand.

Count++ this simple operation according to the above principle analysis, you can know that memory operation is actually divided into read and write storage three steps; Since count is not atomic, two or more threads read the same old value, read it into the thread memory, write it, and store it back again. Then it is possible to have the same set value repeated in the main memory, as shown in the figure above. There’s actually only one valid operation.

Case 2

code

class Foo {
    private int x = 100;

    public int getX(a) {
        return x;
    } 

    public int fix(int y) {
        x = x - y; 
        returnx; }}public class MyRunnable implements Runnable {
    private Foo foo =new Foo(); 

    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread ta = new Thread(r,"Thread-A"); 
        Thread tb = new Thread(r,"Thread-B"); 
        ta.start(); 
        tb.start(); 
    } 

    public  void run(a) {
        
        for (int i = 0; i < 3; i++) {
            this.fix(30);
            try {
                Thread.sleep(1); 
            } catch (InterruptedException e) {
                e.printStackTrace(); 
            } 
            System.out.println(Thread.currentThread().getName() + ": current x value of foo ="+ foo.getX()); }}public int fix(int y) {
        returnfoo.fix(y); }}Copy the code

Output

Thread-a: the current x value of foo = 70 Thread-b: the current x value of foo = 70 Thread-A: the current x value of foo = 10 Thread-b: the current x value of foo = 10 Thread-a: the current x value of foo = -50 thread-b: the current x value of foo = -50Copy the code

Analysis of the

This example is a variant of example 1, but the code is a bit more complicated and convoluted. In fact, there are two threads doing -30 on a shared variable of an instance.

The read operation occurs at x of x-y, which is equivalent to two 100-30 assignments to the x variable when the two threads first fix(30).

Case 3

public class Test {
   	// Is it atomic?
    int i = 1;
    public static void main(String[] args) {
    	Test test = newTest(); }}Copy the code

Int I = 1 is atomic?

It’s subtle, actually.

In this case int a = 1 is explicitly initialized in Java, and it actually contains two assignments. The first time Java automatically initializes a to 0 and the second time it assigns a value of 1. From this point of view, the statement contains two steps and is not atomic.

But because this code is in a constructor, the initialization of the current instance in a constructor is generally considered atomic from the class instantiation point of view. This is because the current instance is generally not accessible from other code until the instantiation is complete. So from this point of view, int a = 1 is actually atomic.

reference

  1. In-depth Understanding of the Java Virtual Machine
  2. Juejin. Cn/post / 684490…
  3. Ifeve.com/concurrency…