Concurrent Programming Topic 03-Principles of Concurrent Programming (Part 1)

Preface

Starting from this section, enter the topic of concurrent programming, a total of 5 sections, namely:

Highlights of this section:

  1. The use and principle of synchronized
  2. wait和notify

synchronized

Synchronized has always been a veteran role in multithreaded concurrent programming, and many people will call it a heavyweight lock. However, with the various optimizations of synchronized in Java SE 1.6, it is not so heavy in some cases. In Java SE 1.6, in order to reduce the performance cost of acquiring and releasing locks, the biased and light The magnitude lock, as well as the storage structure and upgrade process of the lock. We still use the previous case, and then use the synchronized keyword to modify the inc method. Look at the results of the implementation.

public class Demo {
    
    

    private static int count=0;

    public static void inc(){
    
    

        synchronized (Demo.class) {
    
    
            try {
    
    
                Thread.sleep(1);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        for(int i=0;i<1000;i++){
    
    
            new Thread(()->Demo.inc()).start();
        }
        Thread.sleep(3000);
        System.out.println("运行结果"+count);
    }
}

operation result:

Running result 1000

Three application methods of synchronized

There are three ways to lock synchronized, namely

  1. Modification of the instance method, acting on the lock of the current instance, to obtain the lock of the current instance before entering the synchronization code.
  2. The static method acts on the lock of the current class object, and the lock of the current class object must be obtained before entering the synchronization code.
  3. Modify the code block, specify the lock object, lock the given object, and obtain the lock of the given object before entering the synchronization code base.

the object after synchronized brackets

The object after synchronized expansion is a lock. Any object in Java can become a lock. In simple terms, we compare an object to a key. The thread that owns the key can execute this method. After getting the key, During the execution of the method, this key is carried with you, and there is only one. If subsequent threads want to access the current method, they can't access it because they don't have a key. They can only wait at the door and wait for the previous thread to put the key back. Therefore, the synchronized locked object must be the same. If it is a different object, it means that it is the key of a different room, which has no effect on the visitor.

synchronized bytecode instructions

Use javap -v to view the bytecode instructions of the corresponding code. For the implementation of the synchronization block, the monitorenter and monitorexit instructions are used. When we talked about JMM, we mentioned these two instructions. They implicitly executed Lock and UnLock operation is used to provide atomicity guarantee. The monitorenter instruction is inserted at the beginning of the synchronization code block, and the monitorexit instruction is inserted at the end of the synchronization code block. JVM needs to ensure that each monitorenter has a monitorexit corresponding.

These two instructions are essentially acquiring the monitor of an object. This process is exclusive, which means that only one thread can acquire the monitor of the object protected by synchronized at a time.

When the thread executes the monitorenter instruction, it will try to acquire the ownership of the monitor corresponding to the object, that is, try to acquire the lock of the object; and execute monitorexit to release the ownership of the monitor.

The principle of synchronized lock

After jdk1.6, synchronized locks have been optimized, including biased locks, lightweight locks, and heavyweight locks; before understanding synchronized locks, we need to understand two important concepts, one is the object header and the other is the monitor.

Java object header

In the Hotspot virtual machine, the layout of objects in memory is divided into three areas: object header, instance data, and alignment padding; Java object header is the basis for implementing synchronized lock objects. Generally speaking, the lock object used by synchronized is storage In the Java object header. It is the key to lightweight locks and bias locks.

Mawrk Word

Mark Word is used to store the runtime data of the object itself, such as HashCode, GC generation age, lock status flags, locks held by threads, biased thread IDs, biased timestamps, etc. Java object header generally occupies two machine codes (in a 32-bit virtual machine, 1 machine code is equal to 4 bytes, which is 32bit)

Insert picture description here

Insert picture description here

Embodiment in the source code

If you want to have a deeper understanding of the definition of the object header in the JVM source code, you need to care about several files, oop.hpp/markOop.hpp

oop.hpp, each Java Object has a native C++ object oop/oopDesc ​​corresponding to it inside the JVM. First look in oop.hpp

Definition of oopDesc

Insert picture description here
_mark is declared at the top of the oopDesc ​​class, so this _mark can be considered as a header. As we mentioned earlier, the header saves some important status and identification information. There are some notes in the markOop.hpp file to explain the memory layout of markOop

Insert picture description here

Monitor

What is Monitor? We can understand it as a synchronization tool, or it can be described as a synchronization mechanism. All Java objects are natural monitors, and the markOop->monitor() of each object can store the ObjectMonitor object. Analyze the monitor object from the source code level

  • The oopDesc ​​class under oop.hpp is the top-level base class of JVM objects, so each object object contains markOop.

  • markOopDesc ​​in markOop.hpp inherits from oopDesc ​​and extends its own monitor method, which returns an
    ObjectMonitor pointer object.

  • objectMonitor.hpp, in the hotspot virtual machine, the ObjectMonitor class is used to implement the monitor.

Insert picture description here

Synchronized lock upgrade and acquisition process

After understanding the object header and monitor, it will be very simple to analyze the realization of synchronized locks. As mentioned earlier, synchronized locks are optimized, introducing biased locks and lightweight locks; the level of locks is gradually upgraded from low to high, no lock -> biased lock -> lightweight lock -> heavyweight lock.

Spin lock (CAS)

The spin lock is to let the threads that do not meet the conditions wait for a period of time instead of immediately suspending. See if the thread holding the lock can release the lock soon. How to spin? In fact, it is a loop that doesn't make any sense.

Although it avoids the overhead caused by thread switching by occupying the processor time, if the thread holding the lock cannot release the lock soon, the spinning thread will waste processor resources because it will not do anything. Meaningful work. Therefore, there is a limit to the time or number of spin waiting. If the spin exceeds the defined time and the lock is not acquired, the thread should be suspended

Bias lock

In most cases, the lock not only does not have multi-thread competition, but is always acquired by the same thread multiple times. In order to make the thread acquire the lock cheaper, a biased lock is introduced. When a thread accesses the synchronization block and acquires the lock, the thread ID of the lock bias is stored in the lock record in the object header and the stack frame. Later, the thread does not need to perform CAS operations to lock and unlock when entering and exiting the synchronization block. , Just simply test whether there is a bias lock pointing to the current thread stored in the Mark Word of the object header. If the test is successful, it means that the thread has acquired the lock. If the test fails, you need to test whether the mark of the bias lock in Mark Word is set to 1 (indicating that the current bias lock): if not set, use CAS competition lock; if set, try to use CAS The bias lock points to the current thread.

Insert picture description here

Lightweight lock

The main purpose of introducing lightweight locks is to reduce the performance consumption of traditional heavyweight locks using operating system mutexes without multithreading competition. When the bias lock function is turned off or multiple threads compete for the bias lock and the bias lock is upgraded to a lightweight lock, it will try to acquire the lightweight lock.
Insert picture description here

Heavyweight lock

The heavyweight lock is implemented through the monitor inside the object. The essence of the monitor depends on the implementation of the Mutex Lock of the underlying operating system. The switching between threads in the operating system requires a switch from user mode to kernel mode, and the switching cost is very high. high.
When we talked about the Java object header, we talked about the monitor object. In the hotspot virtual machine, the monitor is implemented through the ObjectMonitor class. The implementation of his lock acquisition process will be much simpler.

Insert picture description here

wait和notify

As mentioned in the previous section, wait and notify are two operations used to make the thread enter the waiting state and wake the thread:

Case presentation

Here we use a Demo to demonstrate the process of wait and notify:

Write ThreadWait:

public class ThreadWait extends Thread{
    
    

    private Object lock;

    public ThreadWait(Object lock) {
    
    
        this.lock = lock;
    }

    @Override
    public void run() {
    
    
        synchronized (lock){
    
    
            System.out.println("开始执行 thread wait");
            try {
    
    
                lock.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("执行结束 thread wait");
        }
    }
}

Write ThreadNotify:

public class ThreadNotify extends Thread{
    
    

    private Object lock;

    public ThreadNotify(Object lock) {
    
    
        this.lock = lock;
    }

    @Override
    public void run() {
    
    
        synchronized (lock){
    
    
            System.out.println("开始执行 thread notify");
            lock.notify();
            System.out.println("执行结束 thread notify");
        }
    }
}

Write the test class:

public class TestThread {
    
    

    public static void main(String[] args) {
    
    
        Object lock = new Object();
        ThreadWait threadWait = new ThreadWait(lock);
        threadWait.start();
        ThreadNotify threadNotify = new ThreadNotify(lock);
        threadNotify.start();
    }
}

operation result:

Start execution thread wait
Start execution thread notify
End of execution thread notify End of
execution thread wait

The principle of wait and notify

Calling the wait method will first acquire the monitor lock. After the success is obtained, the current thread will enter the waiting state, enter the waiting queue and release the lock; then when other threads call notify or notifyall, they will choose to wake up any thread from the waiting queue. After the notify method is executed, the thread will not be awakened immediately, because the current thread still holds the lock, and the waiting thread cannot obtain the lock. You must wait until the current thread is executed and press the monitorexit instruction, that is, after the lock is released, the threads in the waiting queue can start competing for the lock.

Insert picture description here

Why do wait and notify need to be in synchronized?
The wait method has two semantics, one is to release the current object lock, and the other is to make the current thread enter the blocking queue, and these operations are related to the monitor, so wait must obtain a monitor lock.

The same is true for notify. It wakes up a thread. Since you want to wake up, you must first know where it is. So it is necessary to find this object to acquire the lock of this object, and then go to the waiting queue of this object to wake up a thread.

Write at the end

Demonstration code address of this section:

https://github.com/harrypottry/ThreadDemo

For more architectural knowledge, please pay attention to this series of articles : The growth path of Java architects

Guess you like

Origin blog.csdn.net/qq_34361283/article/details/109562045