Thread and JMM model

Thread

The concept of threads

When a modern operating system runs a program, it creates a process for it. For example, start a Java program, the operating system will create a Java process. The smallest unit of a modern operating system to schedule a CPU is a thread, also called a lightweight process.

The implementation of threads can be divided into two categories:

  • User-Level Thread
  • Kernel-Level Thread

Thread life cycle

Insert picture description here

Concurrent

在现代多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多
核CPU的计算能力发挥到极致,性能得到提升。

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实
现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通
过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫
秒(ms)

The difference between concurrency and parallelism

并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,
如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能
通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在
拥有多个CPU的系统中。

所以说,并发是一个cpu上通过时间片分配各个线程交替执行。而对于多核CPU,能产生真正的并行。

Advantages of concurrency:

  • Make full use of the computing power of multi-core CPUs
  • Facilitate business splitting and improve application performance

Problems caused by concurrency:

  • High concurrency scenarios, resulting in frequent context switching

  • Critical section thread safety issues, deadlocks are prone to occur, and deadlocks will cause system functions to be unavailable

  • other

    CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片会 
    切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下
    次切换回这个任时,可以再加载这个任务的状态。所以任务从保存到再
    加载的过程就是一次上下文切换。
    

When a thread is executing, if there is waiting or time-consuming io operation or sleep, etc., it will switch the time slice and let the next thread execute. At this time, the thread context will be saved so that the original data can be found when switching back. The online text switching is also a time-consuming operation. If the time slice is over, the program will also be saved to the online text, so that it can run correctly the next time it is executed.

JMM

The "Java Virtual Machine Specification" tried to define a "Java Memory Model" (Java Memory Model for short JMM) to shield the memory access differences of various hardware and operating systems, so that Java programs can be achieved on various platforms. Consistent memory access effect. The Java memory model is an abstract concept and does not really exist. It describes a set of rules or specifications. Through this set of specifications, the variables in the program (including instance fields, static fields, and elements that constitute array objects) are defined. interview method. JMM revolves around atomicity, order, and visibility.

Memory and working memory

Insert picture description here
Main memory

The main storage is the Java instance object. The instance objects created by all threads are stored in the main memory, regardless of whether the instance object is a member variable or a local variable (also called a local variable) in a method. Of course, it also includes shared class information, Constants and static variables. Because it is a shared data area, multiple threads accessing the same variable may cause thread safety issues.

Working memory

Mainly store all the local variable information of the current method (the working memory stores a copy of the variable in the main memory), each thread can only access its own working memory, that is, the local variables in the thread are invisible to other threads, even if The two threads execute the same piece of code, and they will each create local variables belonging to the current thread in their own working memory. Of course, they also include the bytecode line number indicator and related Native method information. Note that since the working memory is the private data of each thread, the threads cannot access the working memory each other, so there is no thread safety issue for the data stored in the working memory.

Eight operations of JMM-memory interaction

Regarding the specific interaction protocol between the main memory and the working memory, that is, how a variable is copied from the main memory to the working memory, and how to synchronize from the working memory back to the main memory, the following 8 operations are defined in the Java memory model To be done. The implementation of the Java virtual machine must ensure that each operation mentioned below is atomic and indivisible (for double and long type variables, load, store, read and write operations are allowed on some platforms exception).

  1. lock : A variable acting on the main memory, which identifies a variable as a thread exclusive state.
  2. unlock : A variable that acts on the main memory. It releases a variable that is in a locked state, and the released variable can be locked by other threads.
  3. read : A variable acting on the main memory. It transfers the value of a variable from the main memory to the working memory of the thread for subsequent load actions.
  4. Load : A variable acting on the working memory. It puts the variable value obtained from the main memory by the read operation into the variable copy of the working memory.
  5. use : A variable acting on the working memory. It passes the value of a variable in the working memory to the execution engine. This operation will be executed whenever the virtual machine encounters a bytecode instruction that needs to use the value of the variable.
  6. assign (assignment) : A variable that acts on the working memory. It assigns a value received from the execution engine to a variable in the working memory. This operation is executed whenever the virtual machine encounters a bytecode instruction that assigns a value to the variable.
  7. store : A variable acting on the working memory. It transfers the value of a variable in the working memory to the main memory for subsequent write operations.
  8. write : A variable acting on the main memory. It puts the value of the variable obtained from the working memory in the store operation into a variable in the main memory.

If you want to copy a variable from main memory to working memory, you need to perform read and load operations in sequence. If you want to synchronize variables from working memory to main memory, you need to perform store and write operations in sequence. However, the Java memory model only requires that the above operations must be performed sequentially, and there is no guarantee that they must be performed continuously.

Insert picture description here
In addition, the Java memory model also stipulates that the following rules must be met when performing the above eight basic operations:

  • One of the read and load, store and write operations is not allowed to appear separately, that is, a variable is not allowed to be read from the main memory but the working memory is not accepted, or the working memory initiates a write back but the main memory does not accept it.
  • A thread is not allowed to discard its latest assign operation, that is, after the variable is changed in the working memory, the change must be synchronized back to the main memory.
  • A thread is not allowed to synchronize data from the thread's working memory back to the main memory for no reason (no assign operation has occurred).
  • A new variable can only be "born" in the main memory. It is not allowed to directly use an uninitialized (load or assign) variable in the working memory. In other words, it must first be used before the use and store operations are performed on a variable. Perform assign and load operations.
  • A variable can only be locked by one thread at the same time, but the lock operation can be repeated multiple times by the same thread. After the lock is executed multiple times, the variable will be unlocked only if the unlock operation is executed the same number of times.
  • If you perform a lock operation on a variable, the value of the variable in the working memory will be cleared. Before the execution engine uses this variable, you need to re-execute the load or assign operation to initialize the value of the variable.
  • If a variable has not been locked by a lock operation in advance, it is not allowed to perform an unlock operation on it, and it is also not allowed to unlock a variable that is locked by other threads.
  • Before performing an unlock operation on a variable, the variable must be synchronized back to the main memory (execute store, write operations).

Visibility, atomicity and order of concurrent programming

Atomicity

Atomicity means that an operation is uninterruptible, even in a multi-threaded environment, once an operation starts, it will not be affected by other threads.

	X=200; //原子性(简单的读取、将数字赋值给变量)
	Y = x; //变量之间的相互赋值,不是原子操作
	X++; //对变量进行计算操作
	X = x+1;

Visibility

After understanding the phenomenon of instruction rearrangement, visibility is easier. When a thread modifies the value of a shared variable, whether other threads can immediately learn the modified value. For serial programs, visibility does not exist, because we modify the value of a variable in any operation, and the value of this variable can be read in subsequent operations, and it is the new modified value.

But in a multi-threaded environment, this is not necessarily true. As we analyzed earlier, since the operations of threads on shared variables are all copied to their respective working memory and then written back to the main memory, there may be a Thread A modifies the value of the shared variable x. When it has not written back to the main memory, another thread B operates on the same shared variable x in the main memory, but at this time, the shared variable x in the working memory of thread A affects thread B. Said it is not visible, this phenomenon of synchronization delay between working memory and main memory causes visibility problems. In addition, instruction rearrangement and compiler optimization may also cause visibility problems. Through the previous analysis, we know whether it is compiler optimization or The processor-optimized rearrangement phenomenon, in a multi-threaded environment, will indeed cause the problem of program execution in turn, which also leads to visibility problems.

Orderliness

Orderliness means that for single-threaded execution code, we always think that the execution of the code is executed sequentially. There is no problem with this understanding. After all, it is true for single-threaded, but for multi-threaded environments, it is possible Out-of-order phenomenon occurs, because the program may be rearranged after the program is compiled into machine code instructions. The rearranged instructions may not be the same as the original instructions. It should be understood that in a Java program, if it is in this thread, All operations are regarded as ordered behavior. If it is a multi-threaded environment, one thread observes another thread, all operations are disordered. The first half sentence refers to the consistency of serial semantic execution in a single thread. Half sentence refers to the phenomenon of instruction rearrangement and synchronization delay between working memory and main memory.

How JMM solves the problem of atomicity & visibility & order

Atomicity problem

In addition to the atomicity of reading and writing basic data types provided by the JVM itself, atomicity can be achieved through synchronized and Lock. Because synchronized and Lock can ensure that only one thread accesses the code block at any time.

Visibility issues

The volatile keyword guarantees visibility. When a shared variable is modified by volatile, it will ensure that the modified value is immediately seen by other threads, that is, the modified value is immediately updated to the main memory. When other threads need to read it, it will go to the memory to read the new value. Synchronized and Lock can also guarantee visibility, because they can ensure that only one thread can access shared resources at any time, and flush the modified variables to memory before releasing the lock.

Order problem

In Java, the volatile keyword can be used to ensure a certain "order". In addition, order can be ensured by synchronized and Lock. Obviously, synchronized and Lock guarantee that one thread executes the synchronization code at each moment, which is equivalent to allowing the threads to execute the synchronization code sequentially, which naturally guarantees order.

The happens-before principle

To put it bluntly, the result of the previous operation is visible to the next operation.
Eight rules

Procedural order rules

The execution result of a piece of code in a thread is orderly. In other words, the instructions will be rearranged, but whatever it is arranged, the result will not change according to the order of our code!
Single-threaded reordering will not affect our execution results.

Title monitor lock rules

That is, whether it is in a single-threaded environment or a multi-threaded environment, for the same lock, after a thread unlocks the lock, another thread acquires the lock, you can see the operation result of the previous thread! (Monitor is a general synchronization primitive, synchronized is the realization of the monitor)
That is to say, each thread executes in the order in which the lock is acquired, regardless of whether it is reordered or not, the acquired data is correct.

Volatile variable rules

That is, if a thread first writes a volatile variable, and then a thread reads the variable, then the result of the write operation must be visible to the read thread.
This is the role of volatile. The shared variable is written first, and the value of this variable is read in another thread after the change. In line with the happens-before principle.

Title thread start rule

During the execution of the main thread A, the child thread B is started, and the result of the modification of the shared variable by the thread A before the child thread B is started is visible to the thread B.

Thread termination rule

During the execution of the main thread A, the child thread B terminates, and the result of the modification of the shared variable by the thread B before the termination is visible in the thread A.

Thread interruption rule

The call to the thread interrupt() method occurs first when the interrupted thread code detects the occurrence of an interrupt event, and it can be detected by Thread.interrupted() whether an interrupt occurs.

Delivery rules

The simple one is that the happens-before principle is transitive, that is, A happens-before B, B happens-before C, then A happens-before C.

Object termination rules

This is also simple, that is, the completion of the initialization of an object, that is, the end of the execution of the constructor must happen-before its finalize() method.

Guess you like

Origin blog.csdn.net/qq_37904966/article/details/112548027