The source of concurrency bugs

In the running of the program, CPU, memory and IO devices (input/output devices, which refers to the hardware that can transmit data with the computer) need to be constantly used, but in the course of the development of the computer, the speed of these three has always been different, among which their speed is CPU> memory > IO.

Because programs frequently use CPU, memory, and IO during running, increasing the speed of one operation alone will not make the whole program faster, and the speed of the program depends on the slowest operation. In order to make reasonable use of the high performance of CPU and balance the speed difference of the three parts, each part has made corresponding optimization, but the optimization of speed has brought bugs in the programming.

Device optimization brings visibility, atomicity, and orderliness issues

  1. CPU cache: Visibility issues were created

Definition: Changes made by one thread to a variable that are visible to another thread are called visibility.

The CPU increases the cache (CPU cache: L1,L2,L3) to balance the speed difference with the memory (when the program sends a memory access request, the CPU will first check whether there is the request data in the cache, if there is, the CPU will directly return, if there is no memory data into the cache and return to the processor. Specific content: CPU cache)

When two threads manipulate variables on different cpus, CPU1’s cache is not visible to CPU2’s cache, so thread 1 makes changes to variable V on CPU1, and thread 2 cannot immediately get the changes to variable V from thread 1, resulting in visibility problems (figure 2).

  1. Time – sharing multiplexing: atomicity problems arise

Definition: The ability for one or more operations to be executed without interruption by the CPU is called atomicity.

When an operating system runs, the process (or thread) runs for a short period of time, the operating system will select a new process (or thread) to run, we call this behavior, the short period of time, we call the time slice.

Add threads and processes in the operating system to time-sharing multiplexing (synchronous time-sharing multiplexing, asynchronous time-sharing multiplexing) CPU, so as to balance the performance gap between CPU and IO. Specific principle: In IO request, there are two operations: read and write, but in communication, waiting is inevitable. In other words, sometimes I/O operations are busy and sometimes idle. In idle times, the CPU can use time division multiplexing to perform other I/O operations, thus reducing the WAITING time for I/OS. That is, when a process (or thread) marks itself idle, it relinquish CPU usage to another process (or thread) and regain CPU usage after waiting for the process (or thread) to wake up.

High concurrency in JAVA is based on multi-threaded operation, so task switching is inevitable during program running. Task switching occurs at the end of the time slice. In high-level languages, a single line of code requires multiple CPU instructions to complete. For example, when i++, the CPU needs at least three instructions to complete.

    1. Load I from memory into the CPU register.
    1. Perform I +1.
    1. Writes the value of I to memory or CPU cache.

In an operating system, task switching can occur at the end of any CPU instruction rather than a line of code in a high-level language. When we start the two threads execute i++, suppose I = 0, when the thread one carries out an instruction 1 task switching, switch to the thread 2, and thread 2 task switching happens in the process of execution, execution after I + 1 operation will I = 1 to write memory or CPU cache, this thread 2 switch to thread one end of the run, Thread 1 executes instruction 2 on register I =0, and writes I =1 back to memory or CPU cache after instruction 3. Therefore, after two threads execute, we find I =1 instead of =2, as shown below.

  1. Compilation optimizations: Order issues arise

Definition: The order in which a program executes code is called orderliness.

Sometimes, in order to optimize the execution speed, the compiler often changes the execution order of the program. Although the final result does not change, it may cause bugs due to task switching in the process of adjusting the execution order of the program.

In Java, there is a double-checked hunchman singleton with the following code:

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(a){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = newSingleton(); }}returninstance; }}Copy the code

At new Singleton(), we thought the order of execution was:

  • 1. Allocate a memory block
  • 2. Initialize the Singleton object on allocated memory
  • 3. Assign the allocated memory address to the instance variable

But the actual order of execution is:

  • 1. Allocate a memory block
  • 2. Assign the allocated memory address to the instance variable
  • 3. Initialize the Singleton object on allocated memory

If a task switch occurs after the execution of instruction 2 and another thread gets the instance variable, instance will be returned directly because the memory address has been assigned to the instance variable, and the instance is not instantiated at this time. Using an uninstantiated instance to fetch other member variables of a Singleton causes a null-pointer problem, which is an order problem when performing optimizations.

Reference Wang Baoling “Java concurrent programming actual combat” Ting Yu notes “dry goods, liver a week of CPU caching foundation” juejin.cn/post/693224…