In Java programming, concurrency problems arise when multiple threads read and write to a variable at the same time. So what is the problem of concurrency, and what causes it?

What is thread safety

When multiple threads access an object, the object is thread-safe if the behavior of calling the object can get the correct result, regardless of scheduling and alternate execution of the threads in the runtime environment, and without additional synchronization or any other coordination on the caller’s side.

Why not make everything thread-safe? There are costs to implementing thread-safe programs, such as the relatively slow execution of thread-safe programs, increased complexity of development, and increased labor costs.

What is the concurrency problem

The concurrency problem is thread insecurity.

When multithreading simultaneously reads and writes a variable, the actual execution result of the variable is inconsistent with the expected result due to atomicity, cache visibility, instruction reordering and other reasons.

Scenario of concurrent problems

Concurrency problems occur when multiple threads simultaneously read and write to a variable. Depending on the variable type and location, there are three scenarios:

  1. Static variable, multithreaded access to the same instance of the class
  2. Static variables, multithreaded access to different instances of the class
  3. Instance member variable. Multiple threads access the same instance

What are the manifestations of concurrency problems

public class Test {
    static int m = 0;
    int n = 0;

    public void inc(a) {
        m++;
        n++;
    }

    public static void inc2(a) { m++; }}Copy the code

In the code above, neither inc() nor inc2() is thread-safe. When two threads hold the same instance of the Test class and execute the inc() method 10,000 times each, the result is not that m and n are both equal to 20,000, but that they are both more than 10,000. When two threads simultaneously execute test.inc2 () 10,000 times, the result is not m equal to 20,000, but more than 10,000.

Causes of concurrency problems

There is a huge speed difference between CPU and peripheral IO and main memory on the computer. In order to improve the utilization rate of CPU computing power and improve the overall system performance, many improvements have been made. In order to solve the problem of slow IO speed of peripherals, the switching function of process and thread is introduced, which causes the atomicity problem of operation. To address the speed difference between CPU and main memory, and to improve CPU computing power, multi-core cpus were introduced, with each kernel having its own separate cache, resulting in cache visibility issues. To improve the efficiency of the CPU’s single kernel pipeline, both the compiler and the kernel reorder the execution order of instructions. The optimization mechanism of these systems causes concurrency problems when multiple threads simultaneously read and write a variable.

1. Atomicity problems caused by thread switching

CPU execution speed is much faster than the response speed of the peripheral IO, in order to improve the CPU execution efficiency, avoid CPU are waiting for IO back most of the time data in the idle state, the operating system process is introduced into the system, due to waiting for IO obstruction, or time out process will be suspended, switch to the other process to continue. In order to further improve the efficiency of process execution and CPU utilization, the system also introduces the concept of thread. Threads can also be scheduled by the system, and thread switching occupies less system resources, so the task of modern system scheduling switch is mainly threads. Processes and threads are essentially increasing the number of concurrent tasks to increase CPU utilization. Atomicity means that an operation is either completely executed or not executed at all. Task switching between threads that concurrently read and write to the same variable can cause concurrency problems if the reads and writes to the variable are not atomic. For example, the following code has concurrency problems when multiple threads are executed concurrently:

int num = 0;
num = num + 1;
Copy the code

Because the second line of code is not an atomic operation, it is actually three instructions:

  1. Read num values from cache or memory and store them in registers
  2. The CPU does +1 on the num value
  3. The CPU writes the result back to cache or memory

May at any time due to system switching threads, if thread 1 after step 1, the system switches to the execution thread 2, then read data and thread one num thread 2 read the data of the same, but the expectations are thread 2 should read the results after the completion of the thread 1, thus causing the concurrency issues, inconsistent results and expected to carry out in practice.

2. Inconsistency caused by multi-core CPU cache

The CPU execution speed is much faster than the memory access speed. Therefore, the system adds a cache between the CPU and memory to improve the CPU utilization. The cache usually reduces the I/O speed to improve the CPU utilization. When reading data, the CPU accesses the cache first. If the cache matches the data, the CPU does not access the main storage and directly returns the data in the cache. When the CPU writes data to the memory, it writes the data to the cache first. Usually, the data is not written back to main storage immediately. The data is written back to main storage only when the cache needs to be replaced or the cache is invalid. Single-core cpus do not have inconsistencies, which only occur with multi-core cpus, which are the majority of modern processors. On multi-core cpus, each kernel has its own separate cache. When multiple threads read and write to the same variable, if the threads are running on different kernels, they will read and write to the same variable in different caches. Each cache is independent and invisible to each other. Operations on variable caches in one cache do not affect other caches, and data in other caches is not known. As a result, the final result is uncontrollable.

3. Instruction reordering problem

In order to improve the execution speed of the kernel itself, the kernel uses pipeline to execute multiple instructions in parallel. There are usually dependencies between instructions caused by data correlation, name correlation and control correlation, which leads to the execution of the later instructions in the pipeline after the completion of the preceding instructions, greatly reducing the parallelism of the pipeline. In order to improve pipeline parallelism performance, compilers and kernels usually schedule instructions statically and dynamically. On the premise of not affecting the execution result of single thread, irrelevant instructions are inserted into the idle position of instruction and executed in advance, so as to improve pipeline performance.

public class Test {
    private static int value;
    private static boolean flag;

    public static void init(a) {
        value = 8;     / / 1
        flag = true;   / / 2
    }

    public static void getValue(a) {
        if(flag) { System.out.println(value); }}}Copy the code

The above code, if executed sequentially by a single thread, must print a value of 8. If init() is reordered, statement 1 will be first and statement 1 second, because statement 1 and statement 2 have no correlation. If this is the case, thread 2 May print an error 0 when thread 1 executes statement 2 and thread 2 executes getValue().

The solution

Java provides the following methods to ensure thread-safety, of two types, depending on whether synchronization is required.

Synchronization scheme: Threads are said to be synchronized if they need to collaborate, that is, if the execution of one thread depends on the results of the execution of another thread. In this case, you can add a mutex in the following two ways:

  1. synchronized:The function and principle of synchronized keyword
  2. LockUse:java.util.concurrent.LockImplementation classes under the interface, such asReentrantLock. As shown in theIntroduction to ReentrantLock and comparison with Synchronized
  3. volatile:The function and mechanism of the volatile keyword

No synchronization solution: However, mutex blocks the execution of other threads, which is inefficient. So if threads do not need to cooperate, that is, one thread’s execution does not depend on the results of another thread’s execution, in which case mutex is not necessary. Java provides the following non-synchronous solution to handle this situation:

  1. ThreadLocal: When multiple threads are working on the same object but do not affect each other and do not need synchronization, mutex can be used to ensure that each thread has a backup of the shared object. Data between threads is isolated from each other, thus avoiding conflicts. See the introduction to ThreadLocal

reference

The basis of concurrency theory: the three root causes of concurrency problems