Introduction: What is the memory layout of objects in memory? How to describe the underlying implementation of synchronized and ReentrantLock and the underlying principles of reentrant? Why is the underlying AQS CAS+ Volatile? How should the four states of a lock and the lock escalation process be described? Object o = new Object() How many bytes in memory? Are spinlocks efficient for specific gravity locks? Will opening bias lock improve efficiency? Where is the weight lock? When is a heavyweight lock more efficient than a lightweight lock, and vice versa? Read on with these questions in mind.

The author source | | ocean pan ali technology to the public

Why do you say this?

After summarizing the AQS, we will review this aspect in passing. This paper starts from the following high-frequency problems:

  • What is the memory layout of objects in memory?
  • Describes the underlying implementations of synchronized and ReentrantLock and the underlying principles of reentrant.
  • Why is the underlying AQS CAS+ Volatile?
  • Describe the four lock states and the lock upgrade process?
  • Object o = new Object() How many bytes in memory?
  • Are spinlocks efficient for specific gravity locks?
  • Will opening bias lock improve efficiency?
  • Where is the weight lock?
  • When is a heavyweight lock more efficient than a lightweight lock, and vice versa?

What happened to the double lock?

Unconscious use of locks:

Println public void println(String x) {synchronized (this) {print(x); newLine(); }}Copy the code

What happened to simple locking?

So to figure out what happens when you lock it you have to look at what the layout looks like in memory after the object is created, right?

After new, an object is divided into four main parts in memory:

  • The markword part is actually the core of the lock, and also contains some life information about the object, such as whether the GC has passed and how many Young GC have survived.
  • Klass Pointer records a class file pointer to an object.
  • Instance Data records variable data in an object.
  • Padding is used for alignment. In the 64-bit server version, the object’s memory must be divisible by 8 bytes. If it is not, it is made up by alignment. For example: New produces an object that takes up only 18 bytes of memory, but requires it to be divisible by 8, so the padding=6.

With these four parts in mind, let’s verify the bottom layer. Use the third-party package JOL = Java Object Layout Java memory Layout to see. You can see the memory layout in a few simple lines:

public class JOLDemo { private static Object o; public static void main(String[] args) { o = new Object(); synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); }}}Copy the code

Print the results:

According to the output results:

1) The object header contains 12 bytes divided into 3 lines, where the first 2 lines are actually markword and the third line is the klass pointer. Note that the output changed from 001 to 000 before and after the lock. Markword uses 8 byte (64bit) header to record some information, lock is modified Markword content (64bit) header to record some information. Changed from 001 without lock to 00 lightweight lock.

2) New produces an object, which takes 16 bytes. The Object header takes up 12 bytes, and since there are no extra variables in Object, instance = 0. Considering that the Object memory size is divisible by 8 bytes, the padding=4, and finally the new Object() memory size is 16 bytes.

Expansion: What kind of objects will enter the old age? Many scenarios such as the object is too large to enter directly, but here I want to explore why the object from the Young GC will enter the Old section after at most 15 experiences while the Young GC is still alive (the age can be adjusted, the default is 15). Mems uses four bits to represent generational ages in the markword graph above, so the maximum range available is 0-15. Therefore, this is why the age of the new generation should not exceed 15, which can be adjusted by -xx :MaxTenuringThreshold at work, but generally we will not change it.

The upgrade process of three locks

1 Verify lock upgrade

Before we explore locking upgrades, let’s do an experiment. Two pieces of code, the difference is that one let it sleep for 5 seconds, one did not sleep. Let’s see if there’s a difference.

public class JOLDemo { private static Object o; public static void main(String[] args) { o = new Object(); synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } } } ---------------------------------------------------------------------------------------------- public class JOLDemo { private static Object o; public static void main(String[] args) { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } o = new Object(); synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); }}}Copy the code

Is there any difference between these two pieces of code? Run and see the result:



Interestingly, the memory layout output after leaving the main thread asleep for 5s is different from that of the output without sleep.

After Syn locking is upgraded, an underlying default in jdk1.8 is 4s after biased locking is enabled. That is to say, there is no biased lock in 4S, and the lock is directly upgraded to lightweight lock.

So how many questions are there?

  • The default syn lock is the heavyweight lock. You can either use it or you can’t?
  • Since 4s if the lock is directly to lightweight, then can not biased lock, why biased lock?
  • Why set the lock bias after 4s?

Question 1: Why do LOCK upgrades need to be done? Lock it, lock it, don’t you need to lock it?

To be clear, early JDK1.2 is very inefficient. At that time, SYN was a heavyweight lock. To apply for a lock, the operating system kernel had to make a system call, join the queue for sorting operation, and then return to the user state after the operation.

Kernel mode: user mode if you want to do some dangerous operations directly access the hardware, it is easy to kill the hardware (formatting, access to the network card, access to memory dry,) the operating system for system security is divided into two layers, user mode and kernel mode. When applying for lock resources, the user mode must apply for lock resources from the operating system kernel mode. In Jdk1.2, the user needs to request the lock from the kernel mode, and the kernel mode also grants the lock to the user mode. This process is very time consuming, resulting in particularly low efficiency early on. Why leave something to the operating system that the JVM can handle? Lock optimization comes from the ability to pull out locks that can be performed by the JVM to improve efficiency.

Question 2: Why biased locking?

In fact, this essentially comes down to a probability problem. Statistics show that in 70% to 80% of the daily use of the SYN lock, generally only one thread to grab the lock, such as system.out. println, StringBuffer, although the underlying syn lock. But there is almost no multi-threaded competition. In this case, there is no need to upgrade to a lightweight lock level. The meaning of bias is that the first thread takes the lock, marks its thread information on the lock, and the next time it comes in, there is no need to take the lock again for verification. If more than one thread tries to grab the lock, the biased lock will be revoked and upgraded to a lightweight lock. In fact, I think biased lock is not a real lock in the strict sense, because biased lock can only happen when one thread accesses the shared resource.

Scenarios where locks are used unintentionally:

/*** public synchronized int length() {return count; } // system.out.println public void println(String x) {synchronized (this) {print(x); newLine(); }}Copy the code

Q3: why does JDK8 turn on bias lock after 4s?

In fact, this is a compromise, knowing that at the beginning of the execution of the code, there must be multiple threads to grab the lock, if the bias lock is turned on efficiency will be reduced, so the above program after 5 seconds sleep bias lock open. The reason why biased locking reduces efficiency is that there are several extra processes in the process. After biased locking is applied, multiple threads will have to upgrade the lock to lightweight lock when fighting for shared resources. In this process, biased lock will be revoked and the efficiency will be reduced. Why 4s? This is a statistical time value.

Of course, biased locking can be disabled by configuring -xx: -usebiasedlocking = false. Biased locking is disabled by default after JDK15. This article is in THE JDK8 environment to do the lock upgrade verification.

2 Lock upgrade process

The process of moving objects into memory from unlocked -> biased locking (if enabled) -> lightweight locking has been verified above. As the lock upgrade process continues, lightweight locks become heavyweight locks. First of all, let’s understand what lightweight locks are, from one thread grabbing resources (biased locks) to multiple threads grabbing resources to lightweight locks, if there aren’t that many threads, it’s actually CAS, which is what we call Compare and Swap. One of the simplest examples of concurrent programming is the atomic operation class AtomicInteger, which follows the packet dispatching. When doing something like ++, the underlying layer is actually the CAS lock.

public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(! this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }Copy the code

Q4: When should a lightweight lock be upgraded to a heavyweight lock?

The first thing we can think about is that the lightweight lock should be unlocked first for multiple threads and then upgraded to heavyweight if it can’t carry. So when does a lightweight lock fail? 1. If there are too many threads, such as 10000 threads, then how long does it take for CAS to switch values? At the same time, CPU light switching between these 10000 threads consumes a lot of resources. So even if you have 10,000 threads that’s dealing with sleep waiting to wake up in a queue. 2. CAS will also be upgraded to heavyweight if it spins 10 times without obtaining a lock.

Overall, there are two cases that upgrade from lightweight to heavyweight, where 10 spins or the number of threads waiting for CPU scheduling exceeds half the number of CPU cores, automatically upgrade to heavyweight locks. To see how many cores the server CPU has, type the top command and press 1.

Q5: Syn is said to be a heavyweight lock, so what is the weight?

The JVM is lazy enough to hand over all thread-related operations to the operating system, such as scheduling lock synchronization directly to the operating system to perform, and in the operating system to perform the first queue, in addition to the operating system to start a thread consumes a lot of resources, the consumption of resources is heavy, heavy here.

The entire lock upgrade process is shown in the figure below:

The low-level implementation of four synchronized

Now that we know a little bit about the memory layout of the object, we know that the lock state is mainly stored in MarkWord. Here we look at the underlying implementation.

public class RnEnterLockDemo { public void method() { synchronized (this) { System.out.println("start"); }}}Copy the code

Reverse parse this simple piece of code and see what happens. javap -c RnEnterLockDemo.class

Monitorenter and Monitorexit appear in the message. We can guess that this is related to lock and unlock instructions. Interesting is 1 Monitorenter and 2 Monitorexit. Why is that? Normally there should be a lock and a release lock. Syn is different from Lock. Syn is a JVC-level lock. If an exception occurs, the JVM releases it automatically, depending on the extra Monitorexit. The lock exception needs to be recovered and released manually.

For the purpose of these two instructions, we refer directly to the JVM specification:

Monitorenter: Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: • If the entry count of the monitor is associated with objectref is zero, The thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. • If the thread already owns the monitor associated with objectref, it reenters the monitor, • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership

Translation:

Each object has a monitor lock. The monitor is locked when it is occupied, and the thread attempts to acquire ownership of the Monitor when it executes the Monitorenter instruction as follows:

  • If monitor’s number of entries is 0, the thread enters Monitor, then sets the number of entries to 1, and the thread is the owner of Monitor.
  • If the thread already owns the monitor and simply re-enters, the number of entries into the monitor is increased by one.
  • If another thread has occupied monitor, the thread blocks until the number of monitor entries is zero, and then tries again to acquire ownership of monitor.

Monitorexit: The thread that executes monitorexit must be The owner of The monitor associated with The instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

Translation:

The thread executing monitorexit must be the owner of the monitor to which objectref corresponds. When the instruction is executed, the number of monitor entries decreases by 1. If the number of monitor entries decreases by 1, the thread exits the monitor and is no longer the owner of the monitor. Other threads blocked by the monitor can try to take ownership of the monitor.

Synchronized is implemented through a monitor object. Wait /notify and other methods also rely on monitor objects. This is why only in the synchronized block or method calls to wait/notify method, otherwise will be thrown. Java lang. Exception IllegalMonitorStateException.

Each lock object has a lock counter and a pointer to the thread that holds the lock.

If the target object’s counter is zero when monitorenter is executed, it is not held by another thread. The Java virtual machine sets the thread holding the lock object to the current thread and increments its counter to I. In the case that the target lock object’s counter is not zero, the Java VIRTUAL machine can increment its counter by one if the holding thread of the lock object is the current thread, or wait until the holding thread releases the lock. When monitorexit is executed, the Java virtual machine decrement the lock object’s counter by one. A counter of zero indicates that the lock has been released.

conclusion

Past experience with synchronized has been that it has become a heavyweight lock. This was true until JDK1.2, when synchronized was optimized because it was too heavy and consumed too much operating system resources. Later can be directly used, as for the strength of the lock, JVM has done the underlying we directly use the line.

Finally, take a look at the first few questions, do you understand? Research with a problem is often clearer. Hope to help you.

The original link

This article is the original content of Aliyun and shall not be reproduced without permission.