Several obvious examples of java concurrency memory model

Three concurrent problems: reordering, memory visibility, atomicity

1. Reorder code example

import java.util.concurrent.CountDownLatch;

public class RearrangeTest {

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (;;) {
            i ++;
            x = 0; y = 0;
            a = 0; b = 0;

            CountDownLatch latch = new CountDownLatch(1);

            Thread one = new Thread(() -> {
                try {
                    latch.await();
                } catch (InterruptedException e) {

                }
                a = 1;
                x = b;
            });

            Thread other = new Thread(() -> {
                try {
                    latch.await();
                } catch (InterruptedException e) {

                }
                b = 1;
                y = a;
            });

            one.start();
            other.start();

            latch.countDown();

            one.join();
            other.join();

            String result = "The first" + i + "second (" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
}

Operation result: the counter common sense result of x=0 and y=0 appears

2. Reordering mechanism

1. Compiler optimization: code reordering during compiler compilation
As in this example, the compiler may transform the code order of a = 1 and x = b, and b = 1 and y = b.
2. Instruction reordering: the CPU changes the code order when executing instructions.
3. Memory system reordering: the memory system does not reorder, but due to the existence of cache, the program as a whole will show out of order behavior. Thread 1 modifies the value of a, but after the modification, the value of a may not be written back to main memory, so thread 2 may get a = 0. Similarly, the assignment operation of thread 2 to b may not be refreshed to main memory in time.

3. Memory visibility

All shared variables exist in main memory, and each thread has its own local memory. Threads read and write shared data through local memory, so the visibility problem still exists.

4. Atomicity

For example, long and double values need to occupy 64 bit memory space. Java can write 64 bit values by splitting them into two 32-bit operations. An overall assignment operation is divided into two operations: low 32-bit assignment and high 32-bit assignment. If other threads read this value in the middle, it may lead to atomicity problems.

5. Java concurrency constraint specification

Synchronization Order
Happens-before Order

VI,synchronized

Thread a is visible to subsequent thread b holding the same monitor lock for operations on shared variables before entering the synchronized block or in synchronized. When entering synchronized, it does not guarantee that the previous write operations can be brushed into the main memory. Synchronized mainly guarantees that the data in the local memory can be brushed into the main memory when exiting.

An incorrect single instance mode double check, the code does not reproduce the multithreading problem, we have to think about another way:

public class Singleton {
        private static Singleton instance = null;
        private int v;

        public int getV() {
            return v;
        }

        public Singleton() {
            this.v = 1;
        }

        public static Singleton getInstance() {     //Singleton mode double check
            if (instance == null) {                 //Operation 1: first inspection
                synchronized (Singleton.class) {    //Operation 2
                    if (instance == null) {         //Operation 3: second inspection
                        instance = new Singleton(); //Operation 4
                    }
                }
            }
            return instance;
        }
 }

For example, there are two threads a and b calling the getInstance() method.
Suppose a goes first and goes all the way to operation 4, that is, the line of code instance = new Singleton(). This line of code first applies for a space, then initializes each attribute to zero value (0/null), performs attribute assignment [1] in the construction method, and assigns the reference of this object to instance[2]. Reordering may occur during [1] and [2].
At this time, when thread b just executes operation 1, it is possible to get that instance is not null, and then thread b will not wait for the monitor lock, but directly return to instance. The problem is that the instance may not have executed the construction method (thread a is still in step 4 at this time), so the instance obtained by thread b is incomplete. The attribute value in it may be the initialized zero value (0/false/null), rather than the value specified by thread a in the construction method.

If all properties are used final In fact, the double check introduced before is feasible and does not need to be added volatile. 

7. volatile: memory visibility and prohibition of instruction reordering

volatile modifier is applicable to the following scenarios: a property is shared by multiple threads, one thread modifies the property, and other threads can get the modified value immediately. In the source code of the contract, it is used very much.
The read and write operations of volatile attribute are lock free, and it cannot replace synchronized because it does not provide atomicity and mutual exclusion. Because there is no lock, it does not need to spend time on acquiring and releasing the lock, so it is low-cost.
Volatile can only act on attributes. We use volatile to modify attributes so that compilers will not reorder this attribute.
Volatile provides visibility, and changes made by any thread will be immediately visible to other threads. The volatile property is not cached by the thread and is always read from main memory.
The read-before variable of vol before of other threads is guaranteed.
volatile can make the assignment of long and double atomic.

VIII,final

1. Classes decorated with final cannot be inherited
2. The method decorated with final cannot be overwritten
3. Attributes modified with final cannot be modified after initialization.

Set in the object's construction method final Property. At the same time, before the initialization of the object is completed, do not write the reference of this object to a place that can be accessed by other threads (do not let the reference escape in the constructor).

Keywords: Java

Added by chauffeur on Sun, 06 Feb 2022 20:15:46 +0200