Demystifying the upgrade of biased locks

Starting today, I will study the principle of synchronized with you in depth. The principle part will involve two articles:

  • The process of upgrading biased locks to lightweight locks
  • The process of upgrading a lightweight lock to a heavyweight lock

Today we will first learn the process of upgrading biased locks to lightweight locks . Because a large number of HotSpot source codes are involved, there will be a separate article on the annotated version of the source code.

Through this article, what do you answer about synchronized questions? The following problems are counted in the statistics:

  • Describe in detail the implementation principle of synchronized
  • Why is synchronized a reentrant lock?
  • Describe in detail the synchronized lock upgrade (expansion) process
  • What is a biased lock? How does synchronized implement biased locks?
  • After Java 8, what optimizations did synchronized do?

Preparation

Before officially starting to analyze the synchronized source code, let's make some preparations:

  • HotSpot source code preparation: Open JDK 11 ;
  • Bytecode tool, jclasslib plug-in is recommended ;
  • The jol-core package for tracking object state .

Tips

  • You can use the javap command and the bytecode tool that comes with IDEA;
  • The advantage of jclasslib is that it can directly jump to the official site of related commands.

sample code

Prepare a simple sample code:

public class SynchronizedPrinciple {

	private int count = 0;

	private void add() {
		synchronized (this) {
			count++;
		}
	}
}

Through the tool, we can get the following bytecode:

aload_0
dup
astore_1
monitorenter // 1
aload_0
dup
getfield #2 <com/wyz/keyword/synckeyword/SynchronizedPrinciple.count : I>
iconst_1
iadd
putfield #2 <com/wyz/keyword/synckeyword/SynchronizedPrinciple.count : I>
aload_1
monitorexit // 2
goto 24 (+8)
astore_2
aload_1
monitorexit // 3
aload_2
athrow
return

The synchronized modified code block is compiled into two instructions:

We noticed that monitorexit appeared twice. The part of Note 2 is the normal execution of the program, and the part of Note 3 is the abnormal execution of the program. The Java team has even considered the abnormal situation of the program for you. He really, I cried to death.

Tips

  • The reason why the synchronized modified code block is used as an example is that it is not intuitive to only set the ACC_SYNCHRONIZED flag in the access_flag when modifying the method;
  • Java does not only exit the monitor through monitorexit, Java once provided a method of entering and exiting the monitor in the Unsafe class
Unsafe.getUnsafe.monitorEnter(obj);
Unsafe.getUnsafe.monitorExit(obj);

Java 8 can be used, and Java 11 has been removed. I don't know the specific removed version.

Example of using jol

Object state can be tracked by jol-core . Maven depends on:

<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.16</version>
</dependency>

Example usage:

public static void main(String[] args) {
	Object obj = new Object();
	System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

start from monitorenter

In HotSpot, the monitorenter command corresponds to these two types of analysis methods:

Since bytecodeInterpreter has basically withdrawn from the stage of history, we take template interpreter X86 to implement templateTable_x86 as an example.

Tips

The execution method of monitorenter is templateTable_x86#monitorenter . In this method, we only need to pay attention to the __lock_object(rmon) executed in line 4438 , and call the interp masm_x86#lock_object method:

void InterpreterMacroAssembler::lock_object(Register lock_reg) {
	if (UseHeavyMonitors) {// 1
		// 重量级锁逻辑
		call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),  lock_reg);
	} else {
		Label done;
		Label slow_case;
		if (UseBiasedLocking) {// 2
			// 偏向锁逻辑
			biased_locking_enter(lock_reg, obj_reg, swap_reg, tmp_reg, false, done, &slow_case);
		}

		// 3
		bind(slow_case);
		call_VM(noreg,   CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),  lock_reg);
		bind(done);
		......
			}

The parts of Note 1 and Note 2 are two JVM parameters:

// 启用重量级锁
-XX:+UseHeavyMonitors
// 启用偏向锁
-XX:+UseBiasedLocking

Note 1 and Note 3, call InterpreterRuntime::monitorentermethod, Note 1 is a configuration that directly uses heavyweight locks, so you can guess that Note 3 is the logic of upgrading the lock to a heavyweight lock after failing to obtain a biased lock.

Object header (markOop)

Before officially starting, let's understand the object header ( markOop ). In fact, markOop's comments have revealed its "secret":

The markOop describes the header of an object.

…..

Bit-format of an object header (most significant first, big endian layout below):

64 bits:

unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)

JavaThread:54 epoch:2 unused:1 age:4 biasedlock:1 lock:2 (biased object)

……

[JavaThread_ | epoch | age | 1 | 01] lock is biased toward given thread

[0 | epoch | age | 1 | 01] lock is anonymously biase

The comment describes in detail the structure of the Java object header in 64-bit big-endian mode :

Tips

Most of the structure in the object header is easy to understand, but what is an epoch?

The comments describe epoch as "used in support of biased locking". Synchronization in the OpenJDK wiki describes epoch like this:

An epoch value in the class acts as a timestamp that indicates the validity of the bias.

Epoch is similar to a timestamp, indicating the validity of the biased lock . It's updated during the bulk rebias phase ( biasedLocking#bulk_revoke_or_rebias_at_safepoint ):

static BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o, bool bulk_rebias, bool attempt_rebias_of_object, JavaThread* requesting_thread) {
	{
		if (bulk_rebias) {
			if (klass->prototype_header()->has_bias_pattern()) {
				klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
			}
		}
	}
}

The JVM uses epoch to judge whether it is suitable for a biased lock. After the threshold is exceeded, the JVM will upgrade the biased lock. JVM provides parameters to adjust this threshold.

// 批量重偏向阈值
-XX:BiasedLockingBulkRebiasThreshold=20
// 批量撤销阈值
-XX:BiasedLockingBulkRevokeThreshold=40

Tips : The update is the epoch of klass.

Biased Locking (biasedLocking)

When the system has opened the biased lock, it will enter the macroAssembler_x86#biased_locking_enter method. The method first gets the markOop of the object:

Address mark_addr         (obj_reg, oopDesc::mark_offset_in_bytes());
Address saved_mark_addr(lock_reg, 0);

I will divide the following process into 5 branches, and analyze the implementation logic of biased locks with you according to the order of execution.

Tips

  • It is enough to understand the bias lock process, so the diagram is the main one, and the source code analysis is placed in the bias lock source code analysis ;
  • The source code analysis of the bias lock is mainly based on comments, and each branch is marked in detail;
  • This part actually includes two jump labels for undo and rebias , which are explained in the branch diagram;
  • The source code uses bitmask technology. In order to facilitate the distinction, the binary number starts with 0B and is filled with 4 digits.

Branch 1: Is it biasable?

The logic of the precondition of the biased lock is very simple, judge the lock flag of the current object markOop , if it has been upgraded, execute the upgrade process; otherwise continue to execute downward.

Tips : The logic of the dotted line is located in other classes.

Branch 2: Is there a reentrant bias?

At present, the JVM knows that the lock flag of markOop is 0B0101, which is in a biased state, but it is not clear whether it has been biased or not yet. HotSopt uses anonymously to describe the state that can be biased but not biased to a certain thread, and this state is called anonymous bias . At this point the object header is as follows:

The thing to do at this time is relatively simple, to determine whether to re-enter the bias lock for the current thread . If it is re-entry, just exit directly; otherwise, continue to execute downward.

Tips : I came across a post today. Javaer and C++er debated reentrant locks and recursive locks. If you are interested, you can read an article to understand locks in concurrent programming. I briefly explained the relationship between reentrant locks and recursive locks.

Branch 3: Is it still biasable?

The comments describe the case of non-reentrant biased locks:

At this point we know that the header has the bias pattern and that we are not the bias owner in the current epoch. We need to figure out more details about the state of the header in order to know what operations can be legally performed on the object's header.

There may be two situations at this point:

  • There is no competition, and a thread is re-biased;
  • There is a race, try to undo.

The partial lock revocation part is a little more complicated. Use the markOop of the object klass to replace the markOop of the object. The key technology is CAS .

Branch 4: Is the epoch expired?

The current state of the biased lock is biasable and biased towards other threads . The logic at this time only needs whether the fragment epoch is valid.

The re-bias can be described in one sentence, build markOop to replace CAS .

Branch 5: Re-biased

The current status of the biased lock is that it can be biased, biased to other threads, and the epoch has not expired . What to do at this time is to set the current thread in markOop, which is the process of re-biasing the bias lock, which is very similar to the part of branch 4.

Undo and Rebias

After failing to obtain the biased lock, execute the InterpreterRuntime::monitorenter method, which is located in the interpreterRuntime :

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
if (UseBiasedLocking) {
	// 完整的锁升级路径
	// 偏向锁->轻量级锁->重量级锁
	ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
	// 跳过偏向锁的锁升级路径
	// 轻量级锁->重量级锁
	ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
IRT_END

ObjectSynchronizer::fast_enter位于synchronizer.cpp#fast_enter

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
	if (UseBiasedLocking) {
		if (!SafepointSynchronize::is_at_safepoint()) {
			// 撤销和重偏向
			BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj,  attempt_rebias, THREAD);
			if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
				return;
			}
		} else {
			BiasedLocking::revoke_at_safepoint(obj);
		}
	}
	// 跳过偏向锁
	slow_enter(obj, lock, THREAD);
}

BiasedLocking::revoke_and_rebiasThe condensed commented version of is placed in Part 2 of the bias lock source code analysis .

Lightweight lock (basicLock)

If the acquisition of the biased lock fails, it will be executed at this time ObjectSynchronizer::slow_enter. The method is located in synchronizer#slow_enter :

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
	markOop mark = obj->mark();
	// 无锁状态 ,获取偏向锁失败后有撤销逻辑,此时变为无锁状态
	if (mark->is_neutral()) {
		// 将对象的markOop复制到displaced_header(Displaced Mark Word)上
		lock->set_displaced_header(mark);
		// CAS将对象markOop中替换为指向锁记录的指针
		if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
			// 替换成功,则获取轻量级锁
			TEVENT(slow_enter: release stacklock);
			return;
		}
	} else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
		//  重入情况
		lock->set_displaced_header(NULL);
		return;
	}
	// 重置displaced_header(Displaced Mark Word)
	lock->set_displaced_header(markOopDesc::unused_mark());
	// 锁膨胀
	ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}

Directly quote the process of lightweight lock locking in "The Art of Java Concurrent Programming":

Before the thread executes the synchronization block, the JVM will first create a space for storing the lock record in the stack frame of the current thread, and copy the Mark Word in the object header to the lock record, which is officially called Displaced Mark Word. Then the thread tries to use CAS to replace the Mark Word in the object header with a pointer to the lock record. If it succeeds, the current thread acquires the lock. If it fails, it means that other threads compete for the lock, and the current thread tries to use spin to acquire the lock.

The logic of lightweight locks is very simple, and the key technology used is also CAS . At this time, the structure of markOop is as follows:

ends at monitor exit

The logic of monitorexit is very simple when it is in a biased lock or a lightweight lock. With the experience of monitorenter, we can easily analyze the calling logic of monitorexit:

  1. templateTable_x86#monitorexit
  2. interp_masm_x86#un_lock
  3. lock exit logic
  4. Biased lock: macroAssembler_x86#biased_locking_exit
  5. Lightweight locking: interpreterRuntime#monitorexit
  6. ObjectSynchronizer#slow_exit
  7. ObjectSynchronizer#fast_exit

The code is left for everyone to explore by themselves, and here is my understanding.

Usually, I simply think that when the biased lock exits, nothing needs to be done (that is, the biased lock will not be actively released) ; for lightweight locks, at least two steps are required:

  • reset displaced_header ;
  • Release the lock record .

Therefore, in terms of exit logic, the performance of lightweight locks is slightly inferior to that of biased locks.

Summarize

Let's make a brief summary of the content of this stage. The logic of biased locks and lightweight locks is not complicated, especially lightweight locks.

The key technology of biased locks and lightweight locks is CAS . When the CAS competition fails, it means that other threads try to snatch it, which leads to lock upgrades.

The biased lock records the thread that holds it for the first time in the object markOop. When the thread continues to hold the biased lock, it only needs a simple comparison. It is suitable for most scenarios and single-threaded execution, but occasionally there may be Scenarios of thread competition.

But the problem is that if the threads are alternately held and executed, the logic of revocation and rebiasing of biased locks is complex and the performance is poor. Therefore, lightweight locks are introduced to ensure the safety of alternating such "slight" race conditions.

In addition, there are many controversies about biased locks, mainly in two points:

  • The revocation of biased locks has a greater impact on performance;
  • When there is a large amount of concurrency, biased locks are very tasteless.

In fact, biased locks have been abandoned in Java 15 ( JEP 374: Deprecate and Disable Biased Locking ), but since most applications are still running on Java 8, we still need to understand the logic of biased locks.

Finally, let’s refute the rumor (or be slapped in the face?), there is no spin logic in lightweight locks .

Tips : It seems that batch undo and batch redirection have been missed~~


If this article is helpful to you, please give it a lot of praise and support. If there are any mistakes in the article, please criticize and correct. Finally, everyone is welcome to pay attention to Wang Youzhi, a financial man . See you next time!

Guess you like

Origin blog.csdn.net/m0_74433188/article/details/132531853