Reasons why multi-threaded concurrent threads are unsafe (explain the reasons, including computer structure and JAVA memory model)


foreword

I believe that many people will encounter such interview questions during interviews, "What is thread-safe and what is not thread-safe". To answer these two questions, you must understand what thread safety is and what the situation is . Will it be thread safe?

Of course, understanding the above questions is not all for interviews. It will also be of great help to your usual code and improve your code quality.


1. Three issues affecting thread safety in concurrent programming

1. Visibility

  • **Concept:** When a thread modifies a shared variable, other threads can immediately see the latest modified value.
  • Demo: Demo visibility issues
    1. Create a shared variable.
    2. Create a thread that continuously reads the shared variable.
    3. Create a thread to modify shared variables.
    4. Expected result: When thread A reads that the flag becomes false, it exits the loop and the program terminates.
  • code:
import java.util.concurrent.TimeUnit;

public class TestSynchronizedPro {
    
    

    // 1. 创建一个共享变量
    public static boolean flag = true;

    public static void main(String[] args) throws Exception {
    
    

        // 开启一个线程读取共享变量的值
        new Thread(() -> {
    
    
            while(flag) {
    
    
                
            }
        }, "A").start();

        TimeUnit.SECONDS.sleep(3);


        // 开启一个线程修改共享变量的值
        new Thread(() -> {
    
    
            flag = false;
            System.out.println("线程" + Thread.currentThread().getName() + "已将flag的值修改为:" + flag);
        }, "B").start();

    }
}

  • Results of the:
    Results of the
  • Summary: From the results, we can see that the shared variable is not read by the A thread after being modified by the B thread. This is the visibility problem in multi-thread concurrent access. The multi-thread code with this problem is thread insecure. , because it will have dirty reads.

2. Atomicity

  • Concept: There are only two cases that can occur in one or more operations,
    • All operations are performed without being interrupted by other factors;
    • All operations are not performed.
  • Demonstration: Demonstrating Atomicity Issues
    1. Define a shared variable number
    2. Start 5 threads
    3. Each thread performs 1000 ++ operations on number.
    4. The final result of number should be 5000.
  • code:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class TestSynchronizedPro {
    
    

    // 1. 定义一个共享变量number
    private static int number;

    public static void main(String[] args) throws Exception {
    
    

        // 每个线程都执行1000次的++操作
        Runnable increament = () -> {
    
    
            for (int i = 0; i < 1000; i++) {
    
    
                number++;
            }
        };

        // 将线程放到集合中,以便拿到最后的结果
        List<Thread> list = new ArrayList<>();

        // 创建5个线程共同操作
        for (int i = 0; i < 5; i++) {
    
    
            Thread t = new Thread(increament);
            t.start();
            list.add(t);
        }

        // join所有线程
        for (Thread thread : list) {
    
    
            thread.join();
        }

        // 打印结果
        System.out.println(number);

    }
}
  • Execution result: We can execute it several times to see the result
    The result of the first execution
    The result of the second execution
    The result of the third execution

  • Explanation: After executing it three times, it can be seen that the expected result is only obtained once. Why is this?

  • Go deep into the bottom layer: To know why the result is like this, we must know what operations the computer performs when it executes number++

    1. To disassemble the .class bytecode file, the command is as follows:
    # 命令参数说明:
    # -p 私有的信息也显示出来
    # -v 输出所有信息
    javap -p -v .\TestSynchronizedPro.class
    
    1. View the assembly information and find the execution code of the lamada expression:
      bytecode instructions
    2. It is not difficult to see from the bytecode instructions in the above figure that number++ corresponds to four instructions, as shown in the figure below:
      Execute the instruction of number++
    3. According to the above figure, when two threads enter the four instructions of number++ at the same time, the two threads get the same value when executing the first instruction (if it is: 0), when a thread executes the last instruction When the value after +1 (that is: 1) is assigned to number, another thread will also assign the value after +1 (also: 1) to number after executing the last instruction, and the result will appear at this time Question, it was supposed to be 2, but it ended up being 1.
  • Summary: During concurrent programming, there will be atomicity problems. When a thread operates half of the shared variable, another thread may also operate the shared variable, which interferes with the operation of the previous thread, resulting in unexpected results. This is one of the reasons why threads are not safe.

3. Ordering

  • Concept: The execution order of the code in the program, JAVA will optimize the code at compile time and runtime, which will cause the final execution order of the program not necessarily the order in which we write the code.
  • Demonstration: Demonstrate ordering problems
    1. Add the jcstress concurrent pressure measurement tool, and add the following information to the pom file:
    <properties>
        <!-- jdk版本 -->
        <javac.target>1.8</javac.target>
        <uberjar.name>jcstress</uberjar.name>
    </properties>
    
    
    <dependencies>
        <dependency>
          <groupId>org.openjdk.jcstress</groupId>
          <artifactId>jcstress-core</artifactId>
          <version>0.15</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.1</version>
            <!-- jar生成路径 -->
            <configuration>
              <compilerVersion>${javac.target}</compilerVersion>
              <source>${javac.target}</source>
              <target>${javac.target}</target>
            </configuration>
          </plugin>
    
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <!-- 将依赖的jar包打包到当前jar包 -->
            <artifactId>maven-shade-plugin</artifactId>
            <version>2.2</version>
            <executions>
              <execution>
                <id>main</id>
                <phase>package</phase>
                <goals>
                  <goal>shade</goal>
                </goals>
                <configuration>
                  <!-- 打包的名字 jcstress.jar -->
                  <finalName>${uberjar.name}</finalName>
                  <!-- 调用下面的两个类 -->
                  <transformers>
                    <transformer
                            implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                      <mainClass>org.openjdk.jcstress.Main</mainClass>
                    </transformer>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                      <resource>META-INF/TestList</resource>
                    </transformer>
                  </transformers>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
    </build>
    
    1. Define two variables.
    2. Simultaneously stress test two methods.
    3. One method assigns values ​​in the stress test object, and one method modifies two variables.
  • Code: The code is as follows:
package com.juc.study;

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;

/**
 * JCStressTest  开启压测工具
 * @Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "OK")
 * 表示我们预期可以接受的结果,分别为 1 和 4 。
 *
 * @Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
 * 表示我们比较感兴趣的结果:0,但这个结果不是我们预期的结果。
 */
@JCStressTest
@Outcome(id = {
    
    "1", "4"}, expect = Expect.ACCEPTABLE, desc = "OK")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class TestJC {
    
    
    // 定义两个变量
    int num = 0;
    boolean ready = false;

    /**
     * 线程1执行的代码
     * @param r
     */
    @Actor
    public void actor1(I_Result r) {
    
    
        if(ready) {
    
    
            r.r1 = num + num;
        } else {
    
    
            r.r1 = 1;
        }
    }

    /**
     * 线程2执行的代码
     * @param r
     */
    @Actor
    public void actor2(I_Result r) {
    
    
        num = 2;
        ready = true;
    }

}

How to use the stress test tool JCStressTest [ Note: Do not put the stress test class in the test directory ]:

  • Common method:
    1. Open the terminal (Terminal)
    2. Enter: mvn clean install
    3. After the packaging is complete, find the jcstress.jar package in the target
    4. Enter: java -jar jcstress.jar
  • Directly use IDEA to operate:
    1. Use maven to package, as shown below:
      Bring up maven and execute
      Packing complete
    2. Configure the running parameters, as shown in the figure below:
      Configuration of JCStressTest tool in IDEA
    3. Click to run, as shown below:
      run directly
  • Execution result: The execution result is as shown in the figure below. From the result, we can see that 1 and 4 appear normally, but 0 also appears. Why?
    Results of the
  • Reason: 0 will appear because in some cases java reorders the code when compiling and running, that is to say, the execution order of the code is adjusted, as shown in the following figure:
    Perform sequential optimization
  • Conclusion: Due to the optimization of JAVA at compile time and run time, the sequence of program codes during execution may not necessarily be the sequence in which developers write codes. This is also the most feared problem encountered in multi-threaded concurrency, and it is also one of the reasons why threads are not safe.

Second, the basis of JAVA program operation

1. Computer structure

1) 5 components of a computer

A computer consists of five major parts, which are:

  • Input devices: keyboard, mouse, microphone, etc.
  • Memory: memory, when the program is running, data will be loaded into the memory for use by the CPU.
  • Output devices: monitors, speakers, printers, etc.
  • Arithmetic unit: It is part of the CPU, the core part of computer operations.
  • Controller: Another part of the CPU, the core part of computer control.

The relationship between each part is as follows:
computer structure

2) Cache

  • Due to the rapid development of the CPU and the relatively high manufacturing process and cost of the memory, the memory cannot keep up with the development speed of the CPU. To solve this problem, a cache is added between the CPU and the main memory.
  • The cache closest to the CPU is L1, followed by L2, L3, and main memory.
  • We can view the cache of our computer from the task manager, as shown in the figure below:
    How to open the task manager
    enter performance
    Cache information
    You can see the three-level cache L1 L2 L3 in the lower right corner

3) The relationship between CPU, cache and memory

  • The relationship between CPU, cache and memory is as follows:
    The relationship between CPU, cache and memory
  • L1: The space is relatively small, the speed is relatively fast, and the price is relatively high.
  • L2: The space is slightly larger than L1, the speed is slightly slower than L1, and the price is slightly cheaper than L1.
  • L3: The space is larger than both L1 and L2, the speed ratio is the slowest, and the price is also the cheapest.
  • Comparison: the following table, from the table we can clearly see the difference between each storage.
    Comparison of each store
  • CPU fetching rules: When the CPU calculates and fetches data, it will first search in L1, if there is one in L1, it will directly fetch the data for processing, and then save it in L1 and memory after processing; if there is no data in L1, it will Find it in L2, and so on, and finally find it from the memory, and save the data to L1 after finding it from the memory.

2. JAVA Memory Model (JAVA Memory Model referred to as JMM)

注意: JAVA内存模型不是JAVA内存结构,两个不要混淆了。

1) Basic definition

  • The JAVA memory model is actually a set of specifications.
  • The whole set of specifications is actually talking about two keywords: synchronized and volatile.
  • It is a model defined in the JAVA virtual machine specification. The JAVA memory model is standardized, shielding the differences between different underlying computers.
  • The JAVA memory model is a set of specifications that describe the access rules of various variables (thread shared variables) in the JAVA program, as well as the underlying details of storing variables to memory and reading variables from memory in the JVM.

2) Basic structure

Let's take a look at the structure of JMM, as shown in the figure below
JAVA memory model
[interpretation]:

  • Divide this model into two parts, one part is 主内存; the other part is工作内存
  • Shared variables in JAVA are placed in the main memory, such as: class member variables (instance variables), static variables (class variables).
  • Accessible by every thread 主内存.
  • Each thread has its own process 工作内存in which the thread executes code .工作内存
  • The thread cannot directly 主内存operate the shared variable in it. It must copy the shared variable and put it 工作内存in it to process the data. After processing, it will synchronize the shared variable back 主内存.

3) The role of JMM

  • When multithreading reads and writes shared data, the rules and guarantees for the visibility, order, and atomicity of shared data (guaranteed by sychronized and volatile) ensure the safety of thread concurrency.
  • The relationship between JMM and real CPU and memory is as follows:
    The relationship between JMM and computer CPU and memory

[Structure Interpretation]:

  • On the left is JMM, the JAVA memory model.
  • On the right is the computer CPU hardware and memory hardware.
  • The working memory of the thread in the JMM can be the register of the computer CPU, the cache of the computer CPU, or the RAM of the computer memory.
  • The main memory in the JMM can be a register of a computer CPU, a computer CPU cache, or a computer memory RAM.

Summarize

The reason for multi-threaded concurrency and thread insecurity needs to understand its underlying principles. This article provides you with a research direction and ideas.

Guess you like

Origin blog.csdn.net/yezhijing/article/details/128272003