Original statement: This article is reprinted from the public account [fat rolling pig learning programming]

One day, the code written by Fat Pig caused a production bug, and the problem was still not solved by 3am. Fat Roll Bear took a look and solved it with a volatile. And told fat pig roll, this is concurrent programming caused by the pit. This makes Fat Rolling pig firm determination to learn concurrent programming. Thus begins our first lesson in concurrent programming.

A prelude to

One source of bugs: visibility

As we mentioned earlier, CPU caching can improve application performance, but caching is also a source of bugs because caching can cause visibility problems. Let’s start with a piece of code:

private static int count = 0; public static void main(String[] args) throws Exception { Thread th1 = new Thread(() -> { count = 10; }); Th2 = new Thread(() -> {system.out. println("count=" + count); }); th1.start(); th2.start(); }Copy the code

It should have returned 10 correctly, but it could have returned 0 instead.

Changes made by one thread to a variable that another thread does not get are bugs caused by visibility. Changes made by one thread to a shared variable that another thread can see immediately are called visibility.

So before we talk about visibility, you need to understand JAVA’s memory model, which I’ve drawn to illustrate:

Main Memory

Main memory is simply the memory of a computer, but it is not exactly the same. Main memory is shared by all threads, and for a shared variable (such as a static variable, or an instance in heap memory), main memory stores its “native” memory.

Working Memory

Working memory can be simply referred to as the CPU cache in a computer, but more accurately it covers caches, write buffers, registers, and other hardware and compiler optimizations. Each thread has its own working memory, where a “copy” of a shared variable is stored.

All operations by a thread on a variable must be done in working memory, rather than directly reading or writing variables in main memory. Threads cannot directly access variables in each other’s working memory, and the transfer of variables between threads needs to be completed through the main memory

Now back to the question of why that code causes visibility problems, based on the memory model, I’m sure you’ll have the answer. When multiple threads execute on different cpus, they operate on different CPU caches. For example, in the following figure, thread A operates on the cache on CPU-1, while thread B operates on the cache on CPU-2

Since threads must do all operations on variables in working memory and cannot read or write variables directly from main memory, for shared variables V, they are first in their own working memory and then synchronized to main memory. But it will not be brushed in time to main memory, but there will be a certain time difference. Obviously, thread A’s operations on variable V are not visible to thread B.

private volatile long count = 0; private void add10K() { int idx = 0; while (idx++ < 10000) { count++; } } public static void main(String[] args) throws InterruptedException { TestVolatile2 test = new TestVolatile2(); Th1 = new Thread(()->{test.add10k (); }); Thread th2 = new Thread(()->{ test.add10K(); }); // Start two threads th1.start(); th2.start(); Th1.join (); // Wait for both threads to finish. th2.join(); System.out.println(test.count); }Copy the code

Original statement: This article is reprinted from the public account [fat rolling pig learning programming]

Atomicity problem

An indivisible operation is called an atomic operation, which is not interrupted by thread scheduling. Once the operation starts, it runs until the end without any thread switching. Note that thread switching is important!

We all know is the allocation of CPU resources are a thread, and time-sharing is invoked, the operating system allows a process to perform a short period of time, such as 50 milliseconds, over 50 milliseconds operating system will choose a process to perform again (we called “task switching”), the 50 milliseconds referred to as the “time slice”. And most of the task switching happens after the time segment ends,

So why is thread switching a bug? Because the operating system does task switching, it can happen at the end of any CPU instruction! Note that this is CPU instruction, CPU instruction, CPU instruction, not a statement in a high-level language. Count++, for example, is a sentence in Java, but a statement in a high-level language often requires multiple CPU instructions to complete. Count++ actually contains three CPU instructions!

  • Instruction 1: First, we need to load the variable count from memory into the CPU register;
  • Instruction 2: After that, the +1 operation is performed in the register;
  • Instruction 3: Finally, write the result to memory (the caching mechanism makes it possible to write to the CPU cache instead of memory).

Javap -c -s testcount.class gets the assembly instructions and verifies that count++ is indeed divided into multiple instructions.

Volatile although can ensure timely execution of the brush variables to the main memory, but for count++ to the non atomicity, multiple instruction situation, because of the thread, as soon as the thread A count = 0 is loaded into the working memory, thread B you can begin to work, this will lead to thread the result of the execution of the A and B are 1, are written in the main memory, The value of main memory is still 1, not 2. The following graph illustrates this process:

Original statement: This article is reprinted from the public account [fat rolling pig learning programming]

Order problem

To optimize performance, JAVA allows the compiler and processor to reorder instructions, sometimes changing the order of statements in a program:

For example, in the program: “A =6; B = 7.” The compiler may be optimized to “b=7; A = 6;” Only in this program does not affect the final results of the program.

Orderliness refers to the order in which the program is executed. But don’t take it for granted, the order here is not the order in which the instructions are executed in order of where the code is located, it means that the end result looks ordered to us.

The process of reordering does not affect the execution of single-threaded programs, but affects the correctness of multi-threaded concurrent execution. Sometimes compiler and interpreter optimizations can lead to unexpected bugs. For example, the classic double check creates a singleton.

public class Singleton { 
 static Singleton instance; 
 static Singleton getInstance(){ 
 if (instance == null) { 
 synchronized(Singleton.class) { 
 if (instance == null) 
 instance = new Singleton(); 
 } 
 } 
 return instance; 
 } 
 }Copy the code

You might think that this program is seamless. I checked for null twice and used synchronized, which, as I said earlier, is an exclusive lock. In general, the logic goes like this: When thread A and thread B enter at the same time, if instance == null, thread A first acquires the lock, and thread B waits. Then thread A will create A Singleton instance and release the lock. After the lock is released, thread B will wake up and try to lock again. When thread B checks instance == null, it will find that it has already created a Singleton instance, so thread B will not create another Singleton instance.

Instance = new Singleton() = new Singleton() = new Singleton() 1. Allocate a block of memory M; 2. Initialize the Singleton object on memory M; 3. Then M’s address is assigned to the instance variable.

If the above three instructions are executed, it is no problem, but after compilation and optimization, the execution path is like this: 1, allocate a block of memory M; 2. Assign M’s address to the instance variable; 3. Finally initialize the Singleton object on memory M

Suppose a thread switch happens when instruction 2 is executed, and the thread is switched to thread B; Thread B also executes the getInstance() method and finds instance! = null, so instance is not initialized. If we call a member variable of instance, we may trigger a null-pointer exception, as shown in the figure below:

conclusion

Concurrent program is a double-edged sword, on the one hand greatly improves program performance, on the other hand brings many hidden invisible bugs that are hard to find. We first need to know what the problem of concurrent programs is. Only by identifying the target can we solve the problem. After all, all solutions are for the problem. The bizarre problems that often occur in concurrent programs may seem absurd, but many concurrency bugs are understandable and diagnosable as long as we have a good understanding of how visibility, atomicity, and orderliness work in concurrent scenarios. To sum up: visibility is caused by caching, thread switching causes atomicity problems, and compilation optimizations cause ordering problems. How to solve it! To find out what happens next, listen next time.

Original statement: This article is reprinted from the public account [fat rolling pig learning programming]