A summary of all the "locks" in java, you will never be afraid of encountering locks in future interviews

1. Introduction

First of all, java locks are divided into two categories:

  1. The first type is the  synchronized  keyword, which is an implicit lock, which is implemented at the JVM level and cannot be seen when used;

  2. The second category is the Lock  interface added after jdk5  and various corresponding implementation classes. This is an explicit lock, that is, we can see the lock object at the code level, and the method implementation of these objects is mostly directly dependent The CPU instruction has nothing to do with the implementation of jvm.

Next, we will talk about  synchronized  and  Lock  .

二、synchronized

2.1 The use of synchronized

  • If the modification is 具体对象: the lock is 对象;

  • If the modification is 成员方法: then the lock is  this ;

  • If the modification is 静态方法: this is the lock 对象.class.

2.2 Java Object Header and Monitor

Before understanding the principle of synchronized, we need to add knowledge of java objects.

The layout of an object in memory is divided into three areas: object header, instance data, and alignment padding .

  1. Object head . The object header part of the Hot Spot virtual machine object includes two types of information. The first type is used to store the runtime data of the object itself, such as Hash Code, GC generation age, lock status flag, lock held by the thread, biased thread ID, biased timestamp, etc., this part of data The length of is 32 bits and 64 bits respectively in 32-bit and 64-bit virtual machines (without compressed pointers). Officially it is called "Mark Word".

The object needs to store a lot of runtime data, which actually exceeds the maximum record of the 32- and 64-bit Bitmap structure. However, the information in the object header is an additional storage cost that has nothing to do with the data defined by the object itself. Taking into account the virtual machine Space efficiency, Mark Word is designed as a dynamically defined data structure to store as much data as possible in a very small space and reuse its own storage space according to the state of the object.

  1. Instance data . The instance data part is the effective information actually stored by the object, that is, the contents of various types of fields defined in the program code, whether inherited from the parent class or the fields defined in the subclass, must be recorded.

The storage order of this part will be affected by the virtual machine allocation strategy parameter (-XX: Fields Allocation Style parameter) and the order in which the fields are defined in the Java source code. The default allocation order of Hot Spot virtual machine is longs/doubles, ints, shorts/chars, bytes/booleans, oops (Ordinary Object Pointers, OOPs). From the above default allocation strategy, you can see that fields with the same width are always Allocate them together and store them. If this prerequisite is met, the variables defined in the parent class will appear before the child class. If the XX: Compact Fields parameter value of the Hotspot virtual machine is true (the default is true), the narrower variables in the subclass can also be inserted into the gap of the parent class variable to save a little space.

  1. Align and fill . It does not necessarily exist, because the automatic memory management system of the Hotspot virtual machine requires that the start address of the object must be an integer multiple of 8 bytes. If the data part of the object instance is not aligned, it needs to be completed by alignment padding.

After introducing the content of the object, what is related to the lock is obviously the content stored in the object header:

  • Among them, the heavyweight lock is also called the synchronized object lock, where the pointer points to the start address of the monitor object (also called monitor or monitor lock). Every object has a monitor associated with it. The monitor is implemented by ObjectMonitor and implemented in C++.

  • Note that there is also a lightweight lock, which is an improvement to the underlying implementation of the synchronized keyword after jdk6.

2.3 Principle of synchronized

We already know that synchronized is related to the instructions in the object header, which is what we used to say:

Java虚拟机可以支持方法级的同步和方法内部一段指令序列(代码块)的同步,这两种同步结构都是使用管程( Monitor,更常见的是直接将它称为“锁”) 来实现的。

Now we talk about the principle.

Because the synchronized modification methods (including ordinary and static methods) and modified code blocks are implemented slightly differently:

1.synchronized modification method

We test a synchronization method:

public class Tues {
    public static int i ;
    public synchronized static void syncTask(){
        i++;
    }
}

Then decompile the class file, you can see:

The method ID:

  • ACC_PUBLIC Represents public modification

  • ACC_STATIC Means static method

  • ACC_SYNCHRONIZED Indicates that the method is a synchronous method.

At this time, we can understand the description of the underlying implementation of the synchronization method in "Deep Understanding of the Java Virtual Machine" as follows:

Method-level synchronization is implicit . No need to control by bytecode instructions, it is implemented in method call and return operations. The virtual machine can learn whether a method is declared as a synchronous method from the ACC_SYNCHRONIZED access flag in the method table structure in the method constant pool. (The same is true for static methods)

  • When the method is called, the calling instruction will check whether the ACC_SYNCHRONIZED access flag of the method is set. If it is set, the execution thread must first successfully hold the monitor (Monitor) before executing the method, and finally when the method is completed (regardless of normal The monitor is released when it is completed or abnormally completed).

  • During the execution of the method, the execution thread holds the monitor, and no other thread can acquire the same monitor.

  • If an exception is thrown during the execution of a synchronized method, and the exception cannot be handled within the method, the monitor held by the synchronized method will be automatically released when the exception is thrown outside the boundary of the synchronized method.

2.synchronized modified code block

Test a piece of synchronization code:

public class Tues {
   public int i;
   public void syncTask(){
       synchronized (this){
           i++;
       }
   }
}

Then decompile the class file:

It can be seen that in terms of instructions, there are more instructions for monitor operation, or the difference from the previous modification method is to use instructions to operate the monitor (Monitor) explicitly.

In the same way, at this time we can understand the description in "A Deep Understanding of the Java Virtual Machine" as follows:

The situation of synchronizing an instruction set sequence . There are two instructions in the instruction set of the Java virtual machine, monitorenter and monitorexit, to support the semantics of the synchronized keyword. (The two instructions of monitorenter and monitorexit are implemented in C language) The correct implementation of the synchronized keyword requires the cooperation and support of the Javac compiler and the Java virtual machine. The implementation of Monitor is basically C++ code, through the operation of JNI (java native interface), interactive programming with cpu directly.

2.4 The problem of early synchronized

Regarding the specific implementation of operating the monitor, we did not go further. The idea of ​​holding monitors, counting, blocking, etc. is similar to using locks directly in java.

The early implementation of synchronized is based on the principle mentioned above, because the monitor lock (monitor) is implemented by the Mutex Lock of the underlying operating system , and the operating system needs to switch from user mode to In the core state , the transition between this state takes a relatively long time and the time cost is relatively high, which is why the early synchronization is inefficient.

More specific overheads also involve the relationship between java threads and operating system kernel threads

When talking about the content stored in the object header, we also left a clue, that is, a lightweight lock is added after jdk6 to improve the implementation of synchronized.

In my understanding, this improvement is: in the process from locking to becoming the previous heavyweight lock, new locks with different states are implemented as a transition.

2.5 Improved various locks

Deflection lock -> Spin lock -> Lightweight lock -> Heavyweight lock . In this order, the weight of the lock increases sequentially.

  • Bias lock . What he means is that the lock will be biased towards the thread that first obtains it. When this thread requests the lock again, it does not need to perform any synchronization operations, thereby improving performance. Then when in the bias lock mode, the Mark Word structure of the object head will change to the bias lock structure.

Research has found that in most cases, locks are not only free from multi-thread competition, but are always acquired by the same thread multiple times. Therefore, in order to reduce the cost of acquiring locks by the same thread, biased locks are introduced. So obviously, once another thread tries to acquire this lock, then the biased mode will end. On the other hand, if most of the program's locks are accessed by multiple threads, then biased locks are redundant.

  • Lightweight lock . When the condition of the biased lock is not met, that is, when there are indeed multiple threads competing for the same lock object concurrently, but the number of concurrency is not large, the lightweight lock is preferred. Generally, when only two threads compete for the lock mark, the lightweight lock is preferred. At this time, the Mark Word structure of the object header will become a lightweight lock structure.

Lightweight locks are compared with traditional heavyweight locks. Traditional locks use the mutex of the operating system, while lightweight locks are updated based on CAS operations by the virtual machine. Try to compare and exchange, and decide according to the situation. Do you want to change to a heavyweight lock. (This dynamic process is also the spin lock process)

  • Heavyweight lock . The heavyweight lock is the lock with the complete Monitor function discussed above .

  • Spin lock . The spin lock is a transitional lock, a transition from a lightweight lock to a heavyweight lock. That is CAS.

CAS, the full name of Compare-And-Swap, is an atomic instruction of the CPU. Its function is to allow the CPU to atomically update the value of a certain position after comparison. The implementation method is based on the assembly instruction of the hardware platform, which means that CAS is implemented by hardware Yes, JVM just encapsulates assembly calls, and those AtomicInteger classes use these encapsulated interfaces.

Note: The various locks in Java are transparent to the programmer: when creating a lock, the JVM first creates the lightest lock, and if the conditions are not met, the lock is upgraded one by one. These four locks can only be upgraded, not downgraded.

2.6 Classification of other locks

The locks mentioned above are all based on the synchronized keyword and the concept of locks involved in the underlying implementation. There are also lock classifications from other perspectives:

According to the characteristics of locks:

  1. Pessimistic lock : An exclusive lock will cause all other threads that need to be suspended, waiting for the thread holding the lock to release the lock, that is to say, it has a pessimistic view, thinking that the pessimistic lock thinks that the concurrent operation of the same data must be Modified. Therefore, for concurrent operations on the same data, pessimistic locking takes the form of locking. For example, as mentioned earlier, the most traditional underlying implementation of synchronized modification, or heavyweight lock. (But now after the synchronized upgrade, it is no longer a purely pessimistic lock)

  2. Optimistic locking : every time it is not locking, but assuming that there is no conflict, the operation is completed tentatively. If it fails due to the conflict, it will retry until it succeeds. For example, the operation of CAS spin locks is not actually locked.

Classified according to the order of locks:

  1. Fair lock . A fair lock means that multiple threads acquire locks in the order in which they apply for locks. In java, you can pass ReentrantLock this lock object, and then specify whether it is fair

  2. Unfair lock . An unfair lock means that the order in which multiple threads acquire locks is not in the order of applying locks. It is possible that the thread that applies later will get the lock first than the thread that applies first. It is impossible to specify fairness or not with synchronized, it is unfair.

Exclusive lock (also called exclusive lock)/shared lock:

  1. An exclusive lock is also called an exclusive lock , which means that the lock can only be held by one thread at a time. Both ReentrantLock and Sychronized are exclusive locks.

  2. Shared lock : means that the lock can be held by multiple threads. For ReentrantReadWriteLock, its read lock is a shared lock and its write lock is an exclusive lock. The shared nature of read locks ensures that concurrent reads are very efficient, and the processes of reading, writing, and writing are mutually exclusive.

Exclusive lock/shared lock is a broad term, mutual exclusion lock/read-write lock is a specific implementation in java.

Three, Lock in Java

As we mentioned above, the lock under the synchronized keyword is implemented at the jvm level. Later, after jdk 5, there is an explicit lock in the juc package . Lock is completely written in Java. At the java level, it has nothing to do with JVM. Achieved. Although Lock lacks the convenience of implicit acquisition and release of locks (provided by synchronized blocks or methods), it has the operability of lock acquisition and release, interruptible acquisition of locks, and timeout acquisition of multiple synchronized keys. Synchronization features that the word does not have.

Lock is an interface. Common implementation classes are:

  • Reentry lock ( ReentrantLock)

  • Read lock ( ReadLock)

  • Write lock ( WriteLock)

The implementation basically completes thread access control by aggregating a subclass of synchronizer ( AbstractQueuedSynchronizer abbreviated as  AQS).

We can look at:

Each lock in it implements the Lock interface, and then you can open a class arbitrarily to find the implementation inside. The operation of Lock relies on the internal class Sync, and Sync inherits the AbstractQueuedSynchronizer class. This class is a very important AQS class.

Overall, the relationship between these classes is quite complicated:

But the general direct use is still very simple, such as a new lock, and then lock and release the lock before and after the required operation.

Lock lock = new ReentrantLock();
lock.lock();//获取锁的过程不要写在 try 中,因为如果获取锁时发生了异常,异常抛出的同时也会导致锁释放
try{

}finally{
    lock.unlock();//finally块中释放锁,目的是保证获取到锁之后最后一定能释放锁。
}

There are 6 methods defined in the Lock interface, and their meanings are as follows:

Next, let's take a look at the commonly used classes step by step.

3.1 AbstractQueuedSynchronizer

Queue synchronizer AbstractQueuedSynchronizer (hereinafter referred to as synchronizer or AQS) is a basic framework used to build locks or other synchronization components. It uses an int member variable to indicate the synchronization status, and completes the queuing work of resource acquisition threads through the built-in FIFO queue .

The main use of the synchronizer is inheritance . Subclasses manage the synchronization state by inheriting the synchronizer and implementing its abstract methods. In the implementation of the abstract method, it is inevitable to change the synchronization state. At this time, you need to use the synchronizer provided Three methods to operate, because they can ensure that the state change is safe.

The three methods are:

  1. protected final int getState(), // Get the current synchronization status

  2. protected final void setState(int newState), // Set the current synchronization status

  3. protected final boolean compareAndSetState(int expect, int update), // Use CAS to set the current state, this method can guarantee the atomicity of state setting

The subclass is recommended to be defined as a static internal class of a custom synchronization component. The synchronizer itself does not implement any synchronization interface. It just defines a number of synchronization state acquisition and release methods for use by the custom synchronization component. The synchronizer can support The synchronization status can be obtained exclusively or shared, so that different types of synchronization components (ReentrantLock, ReentrantReadWriteLock, CountDownLatch, etc.) can be easily implemented.

Three types of template methods defined by AQS;

  1. Exclusive synchronization state acquisition and release

  2. Shared synchronization status acquisition and release

  3. Synchronization status and query the status of waiting threads in the synchronization queue

The built-in FIFO queue of the synchronizer, as you can see from the source code, Node is a container that holds thread references and thread status .

  • Each thread's access to the synchronizer can be regarded as a node in the queue.

  • The node is the basis of the synchronization queue, and the synchronizer has a head node and a tail node;

  • The thread that has not successfully obtained the synchronization status will be the end of the node to join the queue.

  • When the thread of the first node releases the synchronization state, the subsequent node will be awakened, and the subsequent node will set itself as the first node when the synchronization state is successfully obtained.

Because there are a lot of source codes, I won't analyze the specific implementation for now.

3.2 ReentrantLock

  • ReentrantLock is a lock that supports reentry, which means that the lock can support repeated locking of resources by a thread.

  • In addition, the lock also supports fair and unfair choices when acquiring the lock.

ReentrantLock supports fair and unfair choices. The internal implementation mechanism is:

  1. The internal is based on the  AQS realization of a fair and unfair public parent class  Sync (in the code, Sync is an internal class that inherits AQS) for managing synchronization status;

  2. FairSync Inheritance is  Sync used to deal with fairness issues;

  3. NonfairSync Inheritance is  Sync used to deal with unfair issues.

3.3 ReentrantReadWriteLock

At the end of synchronized above, the classification of other dimensions of locks is mentioned:

Exclusive lock (exclusive lock)/shared lock, the specific implementation level corresponds to the mutual exclusion lock/read-write lock in java .

  • ReentrantLock and synchronized are both exclusive locks;

  • ReentrantReadWriteLock
    maintains a read lock and a write lock. The read lock is a shared lock and the write lock is an exclusive lock.

Because of the division of the read-write lock, the ReentrantReadWriteLock lock does not directly implement the Lock interface, its internal is like this:

  • Based on the  AQS realization of a fair and unfair public parent class  Sync , used to manage the synchronization state;

  • FairSync Inheritance is  Sync used to deal with fairness issues;

  • NonfairSync Inheritance is  Sync used to deal with unfair issues;

  • ReadLock Implement  Lock interface, internal aggregation  Sync;

  • WriteLock Implement  Lock interfaces and aggregate internally  Sync.

Four, some summary and comparison

At this point, we know that Java objects have a lock associated with it. This lock is called a monitor lock or an internal lock. It is used through keyword  synchronized declarations. It is actually implemented at the JVM level. The Monitor class is used down. The instructions of the next virtual machine are to deal with the CPU, insert memory barriers, and so on.

After jdk 5 显式的锁, Lock various implementation classes with  interfaces as the core were introduced . They are completely implemented by java. Then the implementation classes are also based on  AQS this queue synchronizer. AQS shields the underlying operations such as synchronization state management, thread queuing and wake-up. Provide template methods and aggregate them into the implementation class of Lock.

Here we compare implicit and explicit locks:

  1. Implicit lock is basically inflexible, because the code block controlled by synchronized cannot cross methods, and the scope of modification is very narrow; while the explicit lock itself is an object, which can give full play to the flexibility of object-oriented, and it can be in one method. Acquire the lock and release it in another method.

  2. Implicit locks are simple and easy to use and will not cause memory leaks; while the process of explicit locks is completely controlled by the programmer, which easily leads to lock leaks;

  3. Implicit locks are only unfair locks; explicit locks support fair/unfair locks;

  4. Implicit locks cannot limit the waiting time and cannot monitor lock information; display locks provide enough methods to complete flexible functions;

  5. Generally speaking, we use implicit locks by default, and only use explicit locks when we need to show the characteristics of the lock.

The comparison is complete  synchronized with  Lock two locks . For the thread synchronization mechanism of java, the other two things that are often mentioned are  volatile keywords and  CAS operations and the corresponding atomic classes.

So here is another mention:

  • volatile The keyword is often called lightweight synchronized, but in fact these two are not the same thing at all. We know that synchronized is implicitly locked by the jvm-level monitor. The volatile keyword is another angle. JVM also adopts corresponding means to ensure:

    • Visibility of the variables modified by it: After the thread modifies the variable, it must be written back to the main memory immediately;

    • When the thread reads the variable, it must read it from the main memory, not the cache;

    • Operations on its modified variables prohibit instruction reordering.

  • CAS It is a CPU instruction and does not belong to locking. It tentatively completes the operation by assuming that there is no conflict. If it fails because of the conflict, it will retry until it succeeds. In fact, we rarely use CAS directly, but java provides some atomic variable classes, which are various Atomicxxx classes in the juc package. The underlying implementation of these classes directly uses CAS operations to ensure that these types of variables are used. Operations are all atomic operations. When using them as shared variables, there is no thread safety issue.


     

Guess you like

Origin blog.csdn.net/bjmsb/article/details/108682242