Dialysis of volatile and synchronized principles from JMM

You will always encounter  volatile and  in interviews, concurrent programming, and some open source frameworks synchronized . synchronized How to ensure concurrency safety? volatile What does semantic memory visibility refer to? What is the relationship with JMM, what is the role of JMM in concurrent programming, and why is JMM needed? How is it different from the JVM memory structure?

Summarize the core knowledge points and the key points of the interview, with pictures and texts, fearless interviews and concurrent programming, and comprehensively improve the internal skills of concurrent programming!

  1. What is the difference between JMM and JVM memory structures?

  2. What exactly is the  JMM (Java Memory Model)  memory model, and what does JMM have to do with concurrent programming?

  3. The most important content of the memory model: instruction rearrangement, atomicity, and memory visibility .

  4. What does volatile  memory visibility refer to? Its application scenarios and pitfall avoidance guides for common mistakes.

  5. Analyze the relationship between the synchronized implementation principle and monitor;

JVM memory and JMM memory model

I will illustrate the JVM memory structure and JMM memory model respectively. I won’t talk too much about JVM here. In the future, there will be articles specifically explaining JVM, garbage collection, and memory tuning. Stay tuned...

Next, we will learn about the JVM memory structure and  JMM memory model through graphics and text  . DJ, trop the beat, lets'go!

The memory structure of the JVM is so coquettish that it needs to be chatted with the data when the virtual machine is running, because the data area where the program runs needs to be divided into different areas.

The Java memory model is also very enchanting and cannot be confused by the JVM memory structure. In fact, it is an abstract definition, mainly for concurrent programming to safely access data.

In summary it is:

  • The JVM memory structure is related to the runtime area of ​​​​the Java virtual machine;

  • The Java memory model is related to concurrent programming in Java.

JVM memory structure

Java code runs on a virtual machine. The .java file we write will be compiled into a .class file first, then loaded by the JVM virtual machine, and translated into the machine code of the corresponding platform according to different operating system platforms, as shown below Show:

JVM cross-platform

As can be seen from the figure, with the abstraction layer of JVM, Java can achieve cross-platform. The JVM only needs to ensure that the .class files can be loaded correctly, and then it can run on platforms such as Linux, Windows, and MacOS.

The JVM loads the class files compiled by javac through the Java class loader, and calls the system interface to realize the running of the program through the execution engine interpretation and execution or JIT instant compilation call.

JVM loading

When the virtual machine runs the program, it will divide the memory into different data areas, and different areas are responsible for different functions. With the development of Java, the memory layout is also being adjusted. The following is the layout after Java 8. Removed For permanent generation, use Mataspace instead, so  -XX:PermSize -XX:MaxPermSize waiting for parameters to become meaningless. The JVM memory structure is shown in the following figure:

JVM memory layout

The module that executes the bytecode is called the execution engine, and the execution engine relies on the program counter to resume thread switching. Local memory contains metadata areas as well as some direct memory.

Heap

The data sharing area stores instance objects and arrays, and usually the largest piece of memory is also shared by data. For example, new Object() will generate an instance; and arrays are also stored on the heap, because in Java, arrays are also objects. The main area of ​​action of the garbage collector.

When that object is created, is it allocated on the heap or on the stack? This has to do with two things: the type of the object and where it exists in the Java class.

Java objects can be divided into basic data types and ordinary objects.

For ordinary objects, the JVM will first create the object on the heap, and then use its reference elsewhere. For example, save this reference in the local variable table of the virtual machine stack.

For basic data types (byte, short, int, long, float, double, char), there are two cases.

We mentioned above that each thread has a virtual machine stack. When you declare an object of a primitive data type in the body of a method, it is allocated directly on the stack. In other cases, it is usually allocated on the heap, and may be allocated on the stack in the case of escape analysis.

Note that things like int[] arrays are allocated on the heap. Arrays are not a primitive data type.

Virtual Machine Stacks (Java Virtual Machine Stacks)

The Java virtual machine stack is based on threads. Even if there is only one main method, it runs in the form of threads. During the running life cycle, the data participating in the calculation will be popped and pushed into the stack, and each piece of data in the "virtual machine stack" It is a "stack frame". When a Java method is executed, a "stack frame" is created and put into the stack "virtual machine stack". When the call ends, the "stack frame" is popped out of the stack, and the corresponding thread also ends.

public int add() {
  int a = 1, b = 2;
  return a + b;
}

The add method will be abstracted into a "stack frame" structure. When the method is executed, the operands corresponding to operands 1 and 2 are pushed onto the stack and assigned to local variables a and b. When the add instruction is encountered, the operation Numbers 1 and 2 are popped from the stack and the result of addition is pushed into the stack. After the method ends, the "stack frame" is popped out of the stack, and the result is returned.

Each stack frame contains four regions:

  1. Local variable table: basic data type, object reference, retuenAddress pointer to bytecode;

  2. operand stack

  3. dynamic link

  4. return address

Here is an important place , knock on the blackboard:

  • In fact, there are two layers of stacks. The first layer is the method corresponding to the "stack frame"; the second layer corresponds to the execution of the method, corresponding to the operand stack.

  • All bytecode instructions will be abstracted into stack push and pop operations. The execution engine only needs to execute in order in a foolish way to ensure its correctness.

Each thread has a "virtual machine stack", each "virtual machine stack" has multiple "stack frames", and a stack frame corresponds to a method. Each "stack frame" contains local variable tables, operand stacks, dynamic links, and method return addresses. The end of the method operation means that the "stack frame" is popped out of the stack.

As shown below:

JVM virtual machine stack

Method Area Metaspace

Store metadata information of each class, such as class structure, runtime constant pool, fields, method data, method constructor, and special methods such as interface initialization.

Is metaspace on the heap?

Answer: It is not allocated on the heap, but allocated in the off-heap space, and the method area is in the meta space.

String constant pool in that area?

Answer: This is different from the different versions of JDK. Before JDK 1.8, the metaspace has not yet formed a group. The method area is placed in a space called the permanent generation, and the string constants are here.

Prior to JDK 1.7, the string constant pool was also placed in a space called the permanent zone. After JDK 1.7, the string constant pool was moved from the permanent generation to the heap.

Therefore, starting from version 1.7, the string constant pool has always existed on the heap.

Native Method Stacks

Similar to the virtual machine stack, the difference is that the former serves Java methods, while the local method stack serves native methods.

Program Counter (The PC Register)

Save the address of the currently executing JVM instruction. Our program is running in thread switching, so how do we know where this thread has been executed?

The program counter is a small memory space that acts as an indicator of the line number of the bytecode being executed by the current thread. What is stored here is the progress of the current thread execution.

JMM (Java Memory Model, Java Memory Model)

DJ, drop the beats! Tick ​​at the heartstrings of the Java memory model.

First of all, it is not a "real existence", but a set of "standards" related to multithreading. It is required that each JVM implementation must abide by such "standards". With the specification guarantee of JMM, concurrent programs run on different virtual machines. The result of the program obtained by the machine is safe, reliable and reliable.

If there is no JMM memory model to standardize, it may appear that after "translation" by different JVMs, the running results will be different and incorrect.

JMM is about processors, caches, concurrency, compilers. It solves the problem of unpredictable data caused by CPU multi-level cache, processor optimization, instruction rearrangement , etc., and ensures that different concurrency semantic keywords are protected by corresponding concurrency and security data resources.

The main purpose is to allow Java programmers to achieve consistent access effects on various platforms.

It is the principle guarantee of the JUC package tool class and the concurrent keyword

volatile、synchronized、Lock etc., their implementation principles all involve JMM. With the participation of JMM, each synchronization tool and keyword can play a role, and the synchronization semantics can take effect, so that we can develop a concurrent and safe program.

The three most important points of JMM: reordering, atomicity, and memory visibility .

instruction reordering

The bug code we wrote, when I thought that the running order of these codes was executed in the order written by my magic pen, I found that I was wrong. In fact, for the purpose of optimizing performance, the compiler, JVM, and even the CPU may not guarantee that the execution order of each statement is consistent with the order of the input code, but adjust the order, which is instruction reordering .

reordering advantage

Maybe we will ask: why do we need to reorder instructions? What's the use?

As shown below:

Java Concurrent Programming 78 Lectures

After reordering, the situation is as shown in the following figure:

Java Concurrent Programming 78 Lectures

After reordering, the instruction for a operation is changed, which saves one load a and one store a , reduces instruction execution, improves speed and changes operation, which is the benefit of reordering.

Three cases of reordering

  • compiler optimization

    For example, Tang Bohu is currently in love with "Qiu Xiang", so it will be much more efficient to put his admiration and dating together for "Qiu Xiang". Avoid going on a date with "Qiuxiang" when you are teasing "Dongxiang", which reduces the time spent on this part. At this moment, we need to rearrange the order in a certain way. However, reordering does not mean that it can be arranged arbitrarily. It needs to ensure that after reordering, the semantics in the single thread will not be changed, and the words said to "Qiuxiang" cannot be passed to "Dongxiang". Otherwise, if they can be arranged arbitrarily, The consequences are disastrous, and the "time management master" is none other than you.

  • CPU reordering

    The optimization here is similar to the compiler, the purpose is to improve the overall operating efficiency by disrupting the order, which is the secret weapon for faster execution.

  • memory "reordering"

    I'm not really reordering, but the result is similar to reordering. Because there is still a difference, I added double quotes as different definitions.

    Due to the existence of memory cache, it appears as main memory and local memory in JMM, and the contents of main memory and local memory may be inconsistent, so this will also cause the program to show out-of-order behavior.

    Each thread can only directly access the working memory and cannot directly operate the main memory, and the data stored in the working memory is a copy of the shared variables of the main memory, and the communication between the main memory and the working memory is controlled by JMM.

for example:

Thread 1 modified the value of a, but did not have time to write the new result back to the main memory after the modification or thread 2 did not have time to read the latest value, so thread 2 cannot see the modification of a by thread 1 just now, at this time thread 2 sees The arrived a is still equal to the initial value. But thread 2 may see the code execution effect after thread 1 modifies a, which seems to be a reordering on the surface.

memory visibility

Let's first look at why there is a memory visibility problem

public class Visibility {
    int x = 0;
    public void write() {
        x = 1;
    }

    public void read() {
        int y = x;
    }
}

Memory visibility problem: When the value of x has been modified by the first thread, but other threads cannot see the modified value.

Suppose two threads execute the above code, the first thread executes the write method, and the second thread executes the read method. Let's analyze what the code looks like in the actual running process, as shown in the following figure:

They can both get this information from main memory, and x is 0 for both threads. But at this time we assume that the first thread executes the write method first, and it changes the value of x from 0 to 1, but its changed action does not happen directly in the main memory, but will happen in the first thread. In the working memory of the thread, as shown in the figure below.

Then, assuming that the working memory of thread 1 has not been synchronized to the main memory, assuming that thread 2 starts to read at this time, then the value of x it reads is not 1, but 0, that is to say, although thread 1 has already transferred the value of x to The value changes, but for the second thread, this change in x is not perceived at all, which creates a visibility problem.

volatile、synchronized、final、和锁 Visibility is guaranteed. It should be noted that volatile, whenever the value of the variable changes, it will be immediately refreshed to the main memory, so other threads want to read this data, they need to be refreshed from the main memory to the working memory.

The lock and synchronization keywords are easier to understand, it is the process of forcing more operations into atomic. Since there is only one lock, variable visibility is easier to guarantee.

atomicity

We can roughly think that the access and read of basic data type variables, reference type variables, and variables of any type declared as volatile are atomic (the non-atomic agreement of long and double: for 64-bit data, such as long and double, The Java memory model specification allows the virtual machine to divide the read and write operations of 64-bit data that are not modified by volatile into two 32-bit operations, which allows the virtual machine to choose load, store, and read that do not guarantee the 64-bit data type. The atomicity of the four operations of and write, that is, if multiple threads share a variable of type long or double that is not declared volatile, and read and modify them at the same time, some threads may read To a value that represents a "half variable" that is neither the original value nor a value modified by another thread.

However, since almost all commercial virtual machines under various platforms choose to treat the read and write operations of 64-bit data as atomic operations, it is generally not necessary to declare the long and double variables used as volatile when writing code) . The reading and writing of these types of variables are naturally atomic, but compound operations like "basic variable ++" / "volatile++" are not atomic. for example i++;

Problems Solved by the Java Memory Model

The three most important points of JMM: reordering, atomicity, and memory visibility . So how does JMM solve these problems?

JMM abstracts two types of main memory (Main Memory) and working memory (Working Memory).

  • Main memory is the area where instances are located, and all instances exist within main memory. For example, fields owned by an instance are located in main memory, which is shared by all threads.

  • Working memory is the working area owned by threads, and each thread has its own dedicated working memory. The working memory stores a copy of the necessary part of the main memory, which is called a working copy (Working Copy).

Threads cannot directly operate on the main memory. As shown in the figure below, if thread A wants to communicate with thread B, it can only exchange through the main memory.

Go through the following 2 steps:

1) Thread A refreshes the updated shared variable in local memory A to the main memory.

2) Thread B goes to the main memory to read the shared variable that thread A has updated before.

JMM memory model

From an abstract point of view, JMM defines the abstract relationship between threads and main memory:

  1. Shared variables between threads are stored in main memory (Main Memory);

  2. Each thread has a private local memory (Local Memory). Local memory is an abstract concept of JMM and does not really exist. It covers caches, write buffers, registers, and other hardware and compiler optimizations. A copy of the thread's read/write shared variables is stored in local memory.

  3. From a lower level, the main memory is the hardware memory, and in order to obtain better operating speed, the virtual machine and hardware system may allow the working memory to be stored in registers and caches first.

  4. The thread's working memory in the Java memory model is an abstract description of the cpu's registers and caches. The JVM's static internal storage model (JVM memory model) is just a physical division of memory. It is only limited to memory, and it is only limited to JVM memory.

eight operations

In order to support JMM, Java defines 8 atomic operations (Action) to control the interaction between main memory and working memory:

  1. read  : Acts on the main memory, and transfers shared variables from the main memory to the working memory of the thread for use in the subsequent load action.

  2. load  : act on the working memory, put the value read by read into the copy variable in the working memory.

  3. Store  storage: acts on the working memory, and transfers the variables in the working memory to the main memory for subsequent write operations.

  4. write  write: act on the main memory, and write the store transfer value to the variable of the main memory.

  5. use  : acts on the working memory, and passes the value of the working memory to the execution engine. When the virtual machine encounters an instruction that needs to use this variable, it will execute this action.

  6. Assign  assignment: acts on the working memory, assigns the value obtained by the execution engine to the variable in the working memory, and executes this operation when the virtual machine stack encounters an instruction to assign a value to the variable. for example int i = 1;

  7. lock (lock)  acts on main memory, marking variables as thread-exclusive.

  8. unlock  acts on main memory, which releases the exclusive state.

In-depth explanation of Java virtual machine

As shown in the figure above, to copy a variable data from the main memory to the working memory, read and load must be executed sequentially; and to synchronize variable data from the working memory back to the main memory, the store and write operations must be executed sequentially.

Due to inconsistencies caused by reordering, atomicity, and memory visibility , JMM uses eight atomic actions and memory barriers to ensure that code with concurrent semantic keywords can achieve corresponding safe concurrent access.

atomicity guarantee

JMM guarantees the atomicity of the six operations of read, load, assign, use, store, and write. It can be considered that except for long and double types, access to memory units corresponding to other basic data types is atomic.

But when you want to use a wider range of atomicity guarantees, you can use the two operations lock and unlock.

Memory Barriers: Memory Visibility and Instruction Reordering

So how does JMM guarantee instruction reordering, and memory visibility brings concurrent access problems?

Memory barriers are used to control reordering and memory visibility issues under certain conditions. JMM memory barriers can be divided into read barriers and write barriers. Java's memory barriers are actually a combination of the above two, completing a series of barrier and data synchronization functions. When the Java compiler generates bytecode, it will insert memory barriers at the appropriate positions in the execution instruction sequence to limit the processor's reordering .

The combination is as follows:

  • Load-Load Barriers: The loading of load1 takes precedence over load2 and all subsequent load instructions. A Load Barrier is inserted before the instruction to invalidate the data in the cache and force the data to be reloaded from the resident memory.

  • Load-Store Barriers: Ensure that the loading of load1 data is flushed to memory before store2 and subsequent storage instructions.

  • Store-Store Barriers: Ensures that store1 data is visible to other processors and precedes store2 and all subsequent store instructions. Inserting the Store Barrie after the Store Barrie instruction will flush the latest data written into the cache to the main memory, making it visible to other threads.

  • Store-Load Barriers: Ensure that Store1 writes are visible to all processors before Load2 and all subsequent read operations are executed. This memory barrier instruction is an all-around barrier, it has the effects of the other three barriers at the same time, and its overhead is also the largest among the four barriers.

JMM summary

JMM is an abstract concept. Due to the multi-core multi-level cache of the CPU and the reason for instruction rearrangement in order to optimize the code, JMM defines a set of specifications in order to shield the details to ensure the ultimate concurrency safety. It abstracts the concept of working memory in main memory, and guarantees atomicity, memory visibility, and prevention of instruction rearrangement through eight atomic operations and memory barriers , so that volatile can guarantee memory visibility and prevent instruction rearrangement, synchronized guarantee In order to ensure memory visibility, atomicity, and prevent thread safety problems caused by instruction rearrangement, JMM is the basis of concurrent programming.

And JMM defines a relationship for all operations in the program, which is called the "Happens-Before" principle. To ensure that the thread executing operation B sees the result of operation A, then "Happens-Before" must be satisfied between A and B relationship, if the two operations lack this relationship, then the JVM can reorder it arbitrarily.

Happens-Before

  • Program sequence principle: If program operation A is before operation B, then the operation in multithreading is still A is executed before B.

  • Monitor lock principle: An unlock operation on a monitor lock must be performed before a lock operation on the same monitor.

  • Principle of volatile variables: The write operation to the variable modified by volatile must be performed before the read operation of the variable.

  • Thread start principle: A call to Tread.start in a thread must be executed before the thread performs any operations.

  • Thread end principle: Any operation of the thread must be executed before other threads detect the end of the thread, or return successfully from Thread.join, or return false when calling Thread.isAlive.

  • Interruption principle: When a thread calls interrupt on another thread, it must be executed before the interrupted thread detects the interrupt call.

  • Finalizer rule: An object's constructor must complete before initiating the object's finalizer.

  • Transitivity: If operation A executes before operation B, and operation B executes before operation C, then operation A must execute before operation C.

volatile

It is a keyword in Java. When a variable is a shared variable and is  volatile modified at the same time, when the value is changed, other threads can ensure that they can get the modified value when they read the variable again, and block all variables through JMM. Different hardware and operating system memory access differences and data inconsistency caused by CPU multi-level cache.

It should be noted that variables modified by volatile are immediately visible to all threads. The keyword itself contains the semantics of prohibiting instruction rearrangement, but it is not safe in concurrent read and write of non-atomic operations. For example, the i++ operation is divided into Three steps.

Compared with  synchronised Lock volatile lighter weight, there will be no overhead such as context switching, and then follow the "code brother byte" to analyze its applicable scenarios and wrong usage scenarios.

The role of volatile

  • Guaranteed visibility: The description of volatile in the Happens-before relationship is as follows: a write operation to a volatile variable happens-before followed by a read operation on the variable.

    This means that if the variable is modified by volatile, after each modification, the latest value of the variable must be read when the variable is read.

  • Prohibition of instruction rearrangement: first introduce the as-if-serial semantics: no matter how reordered, the execution result of the (single-threaded) program will not change. Under the premise of satisfying the as-if-serial semantics, due to the optimization of the compiler or CPU, the actual execution order of the code may be different from the order we wrote, which is no problem in the case of single thread, but once introduced Multi-threading, this disorder may cause serious thread safety problems. This reordering can be prohibited to some extent by using the volatile keyword.

volatile correct usage

boolean flag bit

Shared variables can only be assigned and read, and there are no other multiple compound operations (such as the compound operation i++ that reads data first and then modifies), we can use volatile instead of synchronized or atomic classes, because assignment operations are atomic operations, and volatile also guarantees visibility, so it is thread-safe.

In the following classic scenario  volatile boolean flag, once the flag changes, all threads are immediately visible.

volatile boolean shutdownRequested;

...

public void shutdown() {
    shutdownRequested = true;
}

public void doWork() {
    while (!shutdownRequested) {
        // do stuff
    }
}

During thread 1 executing doWork(), another thread 2 may call shutdown, and thread 1 reads the modified value and stops execution.

A common property of this type of state marking is that there is usually only one state transition ; shutdownRequested the flag false transitions from true, and the program stops.

double check (singleton pattern)

class Singleton{
    private volatile static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if(instance==null) { // 1
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();  //2
            }
        }
        return instance;
    }
}

Why is the volatile keyword needed in double-checked lock mode?

If the Instance class variable is not modified with the volatile keyword, it will cause such a problem:

When the thread executes to line 1 and the code reads that the instance is not null, the object referenced by the instance may not have been initialized yet.

The main reason for this phenomenon is that object creation is not an atomic operation and instruction reordering.

The second line of code can be broken down into the following steps:

memory = allocate();  // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory;  // 3:设置instance指向刚分配的内存地址

Roots are between 2 and 3 in the code and may be reordered. For example:

memory = allocate();  // 1:分配对象的内存空间
instance = memory;  // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象

This reordering may cause the instance obtained by a thread to be non-empty but not fully initialized.

img

The interviewer may ask you, "Why double-check? Is it okay to remove any check?"

Let's look at the second check first. At this time, you need to consider such a situation that two threads call the getInstance method at the same time. Since the singleton is empty, both threads can pass the first if judgment; then Due to the existence of the lock mechanism, one thread will first enter the synchronization statement and enter the second if judgment, while the other thread will wait outside.

However, when the first thread finishes executing the new Singleton() statement, it will exit the area protected by synchronized. At this time, if there is no second if (singleton == null) judgment, then the second thread will also create a Instance, at this time the singleton is destroyed, which is definitely not acceptable.

As for the first check, if it is removed, all threads will execute serially, which is inefficient, so both checks need to be reserved.

volatile incorrect usage

Volatile is not suitable for scenarios that need to ensure atomicity, such as the need to rely on the original value when updating, and the most typical scenario is a++, we cannot guarantee the thread safety of a++ only by volatile. The code looks like this:

public class DontVolatile implements Runnable {
    volatile int a;
    public static void main(String[] args) throws InterruptedException {
        Runnable r =  new DontVolatile();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((DontVolatile) r).a);
    }
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            a++;
        }
    }
}

The final result a < 2000.

synchronized

Mutex synchronization is a common way to ensure the correctness of concurrency. Synchronization is like working in a company. There is only one toilet. Now a group of people want to go to the toilet at the same time for "paid shit".

Mutual exclusion is a means to achieve synchronization, critical section, mutex (Mutex) and semaphore (Semaphore) are the main mutual exclusion methods. Mutual exclusion is the cause, synchronization is the effect.

The monitor lock (Monitor is another name called the monitor) is essentially implemented by relying on the Mutex Lock (mutual exclusion lock) of the underlying operating system. Each object has a monitor associated with it, and there are many ways to implement the relationship between the object and its monitor. For example, the monitor can be created and destroyed together with the object or automatically generated when the thread tries to acquire the object lock. However, when a monitor is Once held by a thread, it is locked.

How mutexes work

In the Java virtual machine (HotSpot), Monitor is implemented based on C++, implemented by ObjectMonitor, several key attributes:

  • _owner: Points to the thread holding the ObjectMonitor object

  • _WaitSet: store the thread queue in wait state

  • _EntryList: store the thread queue in the state of waiting for the lock block

  • _recursions: the number of reentrant locks

  • count: Used to record the number of times the thread acquires the lock

There are two queues in ObjectMonitor, _WaitSet and _EntryList, which are used to save the list of ObjectWaiter objects (each thread waiting for a lock will be encapsulated into an ObjectWaiter object), and _owner points to the thread holding the ObjectMonitor object. When multiple threads access a synchronization When coding, it will first enter the _EntryList collection. When the thread obtains the monitor of the object, it enters the _Owner area and sets the owner variable in the monitor as the current thread and increases the counter count in the monitor by 1.

If the thread calls the wait() method, the currently held monitor will be released, the owner variable will be restored to null, and the count will be decremented by 1. At the same time, the thread enters the WaitSet collection and waits to be woken up. If the current thread finishes executing, it will also release the monitor (lock) and reset the value of the variable so that other threads can enter and acquire the monitor (lock).

In Java, the most basic means of mutual exclusion synchronization is synchronized, which will be inserted before and after the synchronization block after compilation  monitorentermonitorexit these two bytecode instructions, and these two bytecode instructions need to provide a reference type parameter to Specifies the object to be locked and unlocked, as shown below:

  • In ordinary synchronization methods, the reference is associated and locked with the current method instance object;

  • For static synchronization methods, the reference is associated and locked with the class object of the current class;

  • In the synchronous method block, the object specified in the brackets is associated and locked by reference;

Java object header

The lock used by synchronized also exists in the Java object header. In the JVM, the layout of the object in memory is divided into three areas: the object header, instance data, and its filling.

object header

  • Object header: MarkWord and metadata, that is, the object mark and metadata pointer in the figure;

  • Instance object: store the attribute data of the class, including the attribute information of the parent class. If it is the instance part of the array, it also includes the length of the array. This part of memory is aligned by 4 bytes;

  • Padding data: Since the virtual machine requires that the starting address of the object must be an integer multiple of 8 bytes. Padding data does not have to exist, just for byte alignment;

The object header is the key to synchronized implementation. The lock object used is stored in the Java object header. The jvm uses 2 word widths (a word width represents 4 bytes, and a byte is 8bit) to store the object header (if the object If it is an array, 3 words will be allocated, and the extra 1 word will record the length of the array). Its main structure is composed of  Mark Word and Class Metadata Address  .

Mark word records information about objects and locks. When an object is regarded as a synchronization lock by the synchronized keyword, a series of operations around the lock are related to the Mark word.

number of virtual machines object structure illustrate
32/64bit Mark Word Store information such as hashCode, lock information or generational age or GC flag of the object
32/64bit Class Metadata Address The type pointer points to the class metadata of the object, and the JVM uses this pointer to determine which class the object is an instance of.
32/64bit Array length the length of the array (if the current object is an array)

Among them, Mark Word stores the object's HashCode, generational age, lock flag, etc. by default. Mark Word stores different content in different lock states, and the default state in 32-bit JVM is as follows:

lock state 25 bit 4 bit 1 bit Whether it is a biased lock 2 bit lock flag
no lock ObjectHashCode Object generational age 0 01

During the running process, the data stored in Mark Word will change with the change of the lock flag bit, and the following 4 types of data may appear:

The meaning of the lock flag bit:

  1. Lock ID lock=00 means lightweight lock

  2. Lock ID lock=10 means heavyweight lock

  3. Biased lock flag biased_lock=1 means biased lock

  4. Biased lock flag biased_lock=0 and lock flag=01 means no lock status

So far, let's summarize the previous content again. The lock in synchronized(lock) can be represented by any object in Java, and the storage of the lock identifier is actually in the object header of the lock object.

Monitor (monitor lock) is essentially implemented by relying on the Mutex Lock (mutual exclusion lock) of the underlying operating system. The switch of Mutex Lock needs to switch from user mode to kernel mode, so the state transition takes a lot of processor time. So synchronized is a heavyweight operation in the Java language.

Why can any Java object become a lock object?

Each object in Java is derived from the Object class, and each Java Object has a native C++ object oop/oopDesc ​​inside the JVM to correspond. Secondly, when a thread acquires a lock, it actually acquires a monitor object (monitor). The monitor can be considered as a synchronization object, and all Java objects are born with monitors.

When multiple threads access the synchronization code block, it is equivalent to competing for the object monitor to modify the lock identifier in the object. The ObjectMonitor object is closely related to the logic of threads competing for locks.

Summary Discussion

JMM summary

The JVM memory structure is related to the runtime area of ​​​​the Java virtual machine;

The Java memory model is related to concurrent programming in Java. JMM is the basis of concurrent programming. It shields memory access differences caused by hardware and systems, ensures consistency, atomicity, and prohibits instruction rearrangement to ensure safe access. The cached data is invalidated through the bus sniffing mechanism to ensure the visibility of volatile memory.

JMM is an abstract concept. Due to the multi-core multi-level cache of the CPU and the reason for instruction rearrangement in order to optimize the code, JMM defines a set of specifications in order to shield the details to ensure the ultimate concurrency safety. It abstracts the concept of working memory in main memory, and guarantees atomicity, memory visibility, and prevention of instruction rearrangement through eight atomic operations and memory barriers , so that volatile can guarantee memory visibility and prevent instruction rearrangement, synchronized guarantee In order to ensure memory visibility, atomicity, and prevent thread safety problems caused by instruction rearrangement, JMM is the basis of concurrent programming.

synchronized principle

Several concepts of locks are mentioned, such as biased locks, lightweight locks, and heavyweight locks. Before JDK1.6, synchronized was a heavyweight lock with poor performance. Starting from JDK1.6, in order to reduce the performance consumption caused by acquiring and releasing locks, synchronized has been optimized and introduced the concepts of biased locks and lightweight locks.

So starting from JDK1.6, there are four lock states in total. The lock states are from low to high according to the intensity of competition:  no lock state -> biased lock state -> lightweight lock state -> heavyweight lock state . These states will gradually escalate with the lock competition. In order to improve the efficiency of acquiring and releasing locks, locks can be upgraded but not downgraded.

At the same time, in order to improve performance, it also brings lock elimination, lock coarsening, spin lock and adaptive spin lock...

Guess you like

Origin blog.csdn.net/z_ssyy/article/details/128721953