Introduction
synchronized
is a highly complex technology at the JVM level, involving many concepts such as object headers, biased locks, lightweight locks, heavyweight locks, lock biasing, lock inflation, lock revocation, CAS, safe points, and more. The implementation of these technologies varies significantly across different JDK versions.
Synchronized
is a keyword used to mark synchronized code blocks or methods. When a thread enters a code block or method marked with synchronized
, it first attempts to acquire the object lock for the object within the parentheses (for synchronized methods, it is the current class instance). Starting from JDK 1.6, synchronized
locks have a lock upgrade mechanism, which includes biased locks, lightweight locks, and heavyweight locks.
The existence of synchronized
provides a convenient solution for the vast majority of concurrent programming tasks. It relieves developers from the tedious and complex task of thread management, allowing them to focus on the core functionality of the program. However, despite being simple and convenient in everyday use, the internal implementation and upgrade process of synchronized
is highly complex and intricate. In this article, using JDK17u as an example, I will document the exploration and learning process of the underlying mechanisms of synchronized
and its lock upgrade process.
Basic Usage
synchronized
can be used in both methods and code blocks, and the meaning of the lock varies depending on how it is used, including:
- Synchronized method:
synchronized void methodB()
- Static synchronized method:
static synchronized void methodA()
- Object synchronized block:
synchronized(this) {}
- Class synchronized block:
synchronized(Test.class) {}
A summary of the above points:
- There are two main types of locks in
synchronized
: object locks and class locks.- Object lock:
synchronized void methodB()
,synchronized(this) {}
- Class lock:
static synchronized void methodA()
,synchronized(Test.class) {}
- Object lock:
- Object lock: When the same object holds the lock, the same object waits while other objects are unaffected. When different objects hold the lock, they do not affect each other.
- Class lock: When the class lock is held, any object of that class will wait when accessing a static synchronized method, but non-static synchronized methods are unaffected.
- Object locks and class locks do not affect each other. A thread holding an object lock does not impact other threads attempting to acquire the class lock, and vice versa.
Method Locks
Method locks include regular method locks synchronized void methodB()
and static method locks static synchronized void methodA()
. Below is an example:
public class SyncTest {
public static void main(String[] args) {
SyncTest syncTest = new SyncTest();
new Thread(SyncTest::methodA, "Thread 01 ").start();
new Thread(syncTest::methodB, "Thread 02 ").start();
}
// static method lock
static synchronized void methodA() {
System.out.println(Thread.currentThread().getName() + "start");
try {
System.out.println(Thread.currentThread().getName() + "sleep");
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
System.out.println(Thread.currentThread().getName() + "end");
}
// normal method lock
synchronized void methodB() {
System.out.println(Thread.currentThread().getName() + "start");
try {
System.out.println(Thread.currentThread().getName() + "sleep");
Thread.sleep(300);
} catch (InterruptedException ignored) {
}
System.out.println(Thread.currentThread().getName() + "end");
}
}
In the previous summary, we concluded that “object locks and class locks do not affect each other,” so theoretically, the execution of methodA()
and methodB()
does not interfere with each other. This leads to the inference that the outputs of these two threads will definitely be interleaved. After multiple runs and observations, this hypothesis can be verified.
Class Locks and Object Locks
No matter which language or framework is used to design high-concurrency programs, the concept of locks is inevitable. In Java, the concept of locks is complex, with more than 6 different types involved in synchronized
. In the previous general summary, we mentioned that “object locks and class locks do not affect each other,” and we have gained a preliminary understanding through method locks. However, for a more detailed understanding of synchronized blocks, this is still far from enough. Let’s use a simple example of using synchronized blocks to further explain this conclusion:
public class SyncTest {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
SyncTest syncTest = new SyncTest();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5, 5, 5000, TimeUnit.MILLISECONDS,
new SynchronousQueue<>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < 5; i++) {
pool.submit(syncTest::methodA);
}
pool.shutdown();
while (!pool.isTerminated()) {}
System.out.println("execution consumes " + (System.currentTimeMillis() - startTime) + " ms");
}
void methodA() {
synchronized (this) {
// object lock
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread-methodA");
}
}
}
Here, we focus on the method with the object lock block. According to the previous explanation, the execution time of the program should be around (I won’t include the screenshots here because the results are very clear). This is equivalent to serializing the methodA()
method. Next, we add a class lock method:
public class SyncTest {
public static void main(String[] args) {
// ...
ThreadPoolExecutor pool = new ThreadPoolExecutor(
10, 10, 5000, TimeUnit.MILLISECONDS,
new SynchronousQueue<>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < 5; i++) {
pool.submit(syncTest::methodA);
pool.submit(syncTest::methodB);
}
// ...
}
void methodA() {
// ...
}
void methodB() {
synchronized (SyncTest.class) {
// class lock
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread-methodB");
}
}
}
In this case, methodA
uses an object lock, while methodB
uses a class lock. Although the number of tasks submitted to the thread pool has doubled, the program’s execution time is still around . This shows that class locks and object locks are serialized separately.
Four Lock States
So far, we have differentiated between class locks and object locks in synchronized, and we have also understood that the essence of synchronized is to serialize multithreaded tasks. synchronized has four lock states: no lock, biased lock, lightweight lock, and heavyweight lock:
- No Lock: This is the default state of synchronized, indicating that no thread has acquired the lock.
- Biased Lock: If a thread has already acquired the lock and then requests the same lock again, the JVM will not perform the lock acquisition operation again but will mark the lock as a biased lock. This reduces unnecessary lock acquisition operations. Note that biased locks are enabled by default in the JVM after startup and will not be enabled until some time after the JVM starts.
- Lightweight Lock: If multiple threads are competing for the lock, but the competition is not intense (meaning the lock is mostly held by one thread), the JVM will mark the lock as a lightweight lock. Lightweight locks can improve performance when there is little contention.
- Heavyweight Lock: If multiple threads are competing for the lock and the competition is very intense, the JVM will upgrade the lock to a heavyweight lock. A heavyweight lock causes competing threads to enter the BLOCKED state. Other threads can only acquire the lock once the thread holding the lock releases it.
These four lock states in synchronized are automatically managed at the JVM level. A deeper understanding of the synchronized lock upgrade process can help developers better optimize program performance or design better concurrency control strategies.
Lock Upgrade and Downgrade
The process of lock upgrade and downgrade in synchronized is quite complex and involves Java objects, threads, and the JVM. To simplify, let’s simulate a scenario where multiple threads alternately try to acquire the same object’s lock. The Java object starts in the no-lock state. Thread 1 acquires it for the first time, upgrading it to a biased lock. Then, when Thread 2 attempts to acquire the lock, the biased lock is revoked, and it reverts to the no-lock state. When Thread 1 acquires the lock again, it is upgraded to a lightweight lock. Finally, when Thread 2 tries to acquire the lock again, it is upgraded to a heavyweight lock. The specific code level process is as follows:
public class SynchronizedTest {
private static final Object LOCK = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (LOCK) {
System.out.println("Thread 1 acquired the lock.");
try {
// let thread sleeps for a period of time to simulate some complex processing
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Thread 1 released the lock.");
}
});
Thread thread2 = new Thread(() -> {
synchronized (LOCK) {
System.out.println("Thread 2 acquired the lock.");
try {
// let thread sleeps for a period of time to simulate some complex processing
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Thread 2 released the lock.");
}
});
thread1.start();
// sleep for a while to ensure that thread 1 acquires the lock first
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
thread2.start();
// sleeps for a while to ensure that thread 2 tries to acquire the lock but is blocked
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// after this, thread 1 and thread 2 will alternately acquire and release the lock in an unknown order.
}
}
This process is based on the theory that “the upgrade of synchronized only occurs when an attempt to acquire the lock fails.” The lock upgrade process in synchronized is an optimization strategy aimed at minimizing system overhead under different contention conditions. Lock upgrades generally happen under the following conditions (before a lock upgrade, a lock expansion will occur):
- No Lock to Biased Lock: When a thread first accesses a synchronized block, the JVM records the thread ID in the object header, and the thread will hold the biased lock. This lock favors the first thread that accesses the lock, and if the same thread accesses the synchronized block again, no synchronization operations are needed. This upgrade process automatically begins a few seconds after the program starts.
- Biased Lock to Lightweight Lock: If a new thread attempts to acquire an object that is protected by a biased lock, the JVM will first check whether the thread ID in the object header matches the current thread. If it does not, the JVM will attempt to revoke the biased lock and upgrade it to a lightweight lock. If the original thread is still active during the revocation or if another thread attempts to acquire the lock, the lock may directly be upgraded to a heavyweight lock.
- Lightweight Lock to Heavyweight Lock: If a thread attempts to acquire a lightweight lock but finds that the lock is already held by another thread, it will enter a spinning state, repeatedly trying to acquire the lock. If the spinning exceeds a certain number of attempts (the specific number depends on the JVM), the lock will be upgraded to a heavyweight lock. At this point, any other threads attempting to acquire the lock will be blocked until the lock is released.
Low-Level Implementation
The implementation of synchronized locks seems quite complex, and most developers don’t need to dive too deeply into it. However, if you want to explore synchronized in detail, you need to look into the Hotspot source code. Taking JDK17u as an example, first locate the implementations of lock states, lock acquisition processes, and lock safety checks:
- Biased Lock:
src/hotspot/share/runtime/biasedLocking.cpp
- Lightweight and Heavyweight Locks:
src/hotspot/share/runtime/synchronizer.cpp
- Lock Expansion and Revocation:
src/hotspot/share/runtime/synchronizer.cpp
- Safepoints:
src/hotspot/share/runtime/safepoint.cpp
- Safepoint Polling:
src/hotspot/share/runtime/safepointMechanism.cpp
Object Header
Before analyzing each part in detail, it is important to understand the theoretical foundation of the object header. Synchronized uses part of the object header’s flag bits to implement locking, which has three bits and can represent four states: biased lock, lightweight lock, heavyweight lock, and no lock. The concept of the object header is simple, so it will not be explained further here.
The logic for manipulating the object header and attempting to acquire a lock is located in the synchronizer.cpp
file. Below is the source code with official comments removed and my own understanding added:
void ObjectSynchronizer::enter(Handle obj, BasicLock* lock, JavaThread* current) {
if (obj->klass()->is_value_based()) {
handle_sync_on_value_based_class(obj, current);
}
// determine whether it is already a biased lock
if (UseBiasedLocking) {
BiasedLocking::revoke(current, obj); // try to revoke the biased lock
}
// get the mark word of the object header, where the lock information is stored
markWord mark = obj->mark();
assert(!mark.has_bias_pattern(), "should not see bias pattern here");
// determine whether the lock is in unlocked state
if (mark.is_neutral()) {
lock->set_displaced_header(mark);
// attempt to perform a CAS operation and
// set the object header to a lightweight lock
if (mark == obj()->cas_set_mark(markWord::from_pointer(lock), mark)) {
return;
}
} else if (mark.has_locker() &&
current->is_lock_owned((address)mark.locker())) {
// the state of the object header indicates that it is locked
// and the owner of the lock is the current thread
assert(lock != mark.locker(), "must not re-lock the same lock");
assert(lock != (BasicLock*)obj->mark().value(), "don't relock with same BasicLock");
lock->set_displaced_header(markWord::from_pointer(NULL));
return;
}
lock->set_displaced_header(markWord::unused_mark());
// if none of the above are met, the heavyweight lock processing flow will be entered
while (true) {
// upgrade the lock to a heavyweight lock
ObjectMonitor* monitor = inflate(current, obj(), inflate_cause_monitor_enter);
// call ObjectMonitor's enter function
// to try to acquire the heavyweight lock
if (monitor->enter(current)) {
return;
}
}
}
The enter
function is used in the JVM to handle the process of a thread attempting to acquire an object lock, and it also manages the logic for biased locks, lightweight locks, and heavyweight locks. Let’s break down and analyze this code.
Biased Lock
Starting from JDK 15, Java officially deprecated the use of biased locking, and it was completely removed in JDK 17u. This means that Java no longer enables biased locking by default. In the following, we’ll first examine biased locks in JDK 1.8, and then analyze why biased locks were removed.
In a biased lock, two additional flag bits are used in the object header: one indicates whether the lock can be biased, and the other indicates whether it has been biased. In Hotspot, biased locking is enabled by default, and it can be controlled using the startup parameters -XX:+UseBiasedLocking
and -XX:-UseBiasedLocking
. After an object is instantiated, it is by default in a biased state. When a thread first acquires the biased lock for this object, the thread’s ID is set in the object header, indicating that the lock is now biased toward this thread.
In JDK 1.8, the BiasedLocking::revoke_and_rebias
function is primarily responsible for this process. It first tries to revoke the biased lock and then rebias the lock.
bool BiasedLocking::revoke_and_rebias(Handle obj, bool is_bulk, JavaThread* requesting_thread) {
markWord mark = obj->mark();
// check the markWord of object to determine the state of lock
assert(!mark.has_bias_pattern() || (mark.age() == markWord::max_age), "should have been aged to prevent inflation");
markWord biased_prototype = markWord::biased_locking_prototype()->set_age(mark.age());
// decide how to revoke the existing lock and set a new biased lock based on current state
if (mark.is_neutral()) {
obj->set_mark(biased_prototype);
return true;
}
if (mark.has_bias_pattern()) {
if (mark.biased_locker() == requesting_thread) {
return false;
}
}
// ...
obj->set_mark(biased_prototype);
return true;
}
In JDK 17u, the main functions handling biased locks are BiasedLocking::revoke_at_safepoint
and BiasedLocking::revoke
. From the function names, it’s clear that these functions do not support biased lock acquisition; instead, they are responsible for revoking biased locks on objects that are in biased lock mode:
- The
BiasedLocking::revoke
function iterates through all the objects passed in and checks if each object’smarkWord
is in biased lock mode. If it is, it calls thewalk_stack_and_revoke
function to revoke the biased lock. Finally, if any biased locks were revoked, it calls theclean_up_cached_monitor_info
function to clean up cached monitor information. - The
BiasedLocking::revoke_at_safepoint
function also revokes biased locks, but it is called at a JVM safepoint, when all Java threads are paused. The function first checks the heuristic rules for biased lock revocation, and based on those rules, it decides whether to perform a single revocation (single_revoke_at_safepoint
) or a bulk revocation or rebiasing (bulk_revoke_at_safepoint
). After the biased lock is revoked, the cached monitor information is also cleaned up.
Since biased locking has been completely deprecated in JDK 17, there’s no further need to study the underlying biased lock implementation in JDK 17u. Now let’s take a look at why Java decided to deprecate biased locks starting from JDK 15:
- In the past, Java applications often used older collection libraries like
HashTable
andVector
, which heavily relied onsynchronized
to ensure thread safety. However, these libraries are no longer recommended by the official Java documentation, as better performing and feature-rich collection classes were introduced in JDK 1.2. - Biased locks were introduced by the Java HotSpot VM to optimize multithreaded lock contention, but in practical use, biased locks did not always provide the expected performance benefits.
- Specifically, biased locks were designed to optimize locks that experience little to no real contention, by saving unnecessary overhead typically produced when there is no contention. However, in scenarios with lock contention, biased locks could cause additional overhead. For example, when a thread attempts to acquire a biased lock and it is not the original biased thread, the system needs to revoke the biased lock, leading to extra system overhead.
- As modern multi-core processors continue to enhance parallelism, the use of lock-free programming models and other concurrent tools and frameworks has become increasingly common. These factors made the performance benefits of biased locking less noticeable. Additionally, biased locking increased JVM complexity and could lead to hard-to-diagnose issues.
- After weighing the pros and cons, Java decided to deprecate biased locking in JDK 15. The specific reasons for deprecating it can be found in JEP 374, which also mentions a related point concerning HotSpot:
- Biased locks introduced significant complexity to the entire “synchronization subsystem,” which also infiltrated other components of HotSpot.
- This made the system code harder to understand, difficult to change, and hindered the evolution of the subsystem.
Lightweight Lock
Returning to the source code in the object header, the obj()->cas_set_mark(markWord::from_pointer(lock), mark)
function implements the setting of a lightweight lock. From the function name, it’s clear that CAS operations are used to implement lock upgrades, so let’s dive into this part of the code:
markWord oopDesc::cas_set_mark(markWord new_mark, markWord old_mark) {
// CAS Handle
uintptr_t v = HeapAccess<>::atomic_cmpxchg_at(as_oop(), mark_offset_in_bytes(), old_mark.value(), new_mark.value());
// return CAS result wrapped by markWord
// 返回被设置为CAS结果的markWord
return markWord(v);
}
When synchronized
tries to acquire a lightweight lock, if the object’s mark word is in a neutral state (unlocked state), it will attempt to use a CAS operation to change the mark word to the lightweight lock state (setting the mark word to point to a BasicLock
pointer). This process is tentative, and if the CAS operation fails, indicating that another thread is trying to acquire the lock at the same time, the synchronized
block may selectively upgrade to a heavyweight lock (this decision is made by the JVM in HotSpot).
markWord
The src\hotspot\share\oops\markWord.hpp
file is part of the underlying object header in Java objects, containing runtime data about the object itself, such as hash code (HashCode), GC age, lock status flags, and more. The content and structure of the markWord
may vary depending on the object. For a 64-bit system, a markWord
roughly contains the following structure:
- Unlocked state (default state):
- No biased lock: GC age (4 bits) + type pointer to the class metadata address (54 bits) + biased lock flag (1 bit) + lock flags (2 bits)
- Biased lock: thread ID (54 bits) + epoch (2 bits) + biased lock flag (1 bit) + lock flags (2 bits)
- Lightweight lock state:
- Pointer to the stack lock record (62 bits) + lock flags (2 bits)
- Heavyweight lock state:
- Pointer to the heavyweight lock (i.e., monitor) (62 bits) + lock flags (2 bits)
- GC mark state:
- Some marking information used for garbage collection algorithms
// ......
static const int age_bits = 4;
static const int lock_bits = 2;
static const int biased_lock_bits = 1;
static const int max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits;
static const int hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits;
static const int unused_gap_bits = LP64_ONLY(1) NOT_LP64(0);
static const int epoch_bits = 2;
// ......
Taking the lightweight lock upgrade process as an example, the cas_set_mark
method uses a CAS operation to modify the object’s mark word, ensuring atomicity in a multithreaded environment and preventing data races. For instance, in the implementation of a lightweight lock (upgrading from biased lock), the thread ID field in the mark word is replaced with a pointer to the lock record. If the CAS operation succeeds, it indicates that the lock acquisition was successful; if it fails, it means that another thread is attempting to acquire the lock, and the thread will then need to enter either a spinlock or a blocked state.
In the implementation of biased locks, a similar approach is used to modify the thread ID field in the mark word, setting it to the current thread’s ID, indicating that the object is biased towards the current thread.
Heavyweight Lock
Returning to the object header code, the lightweight lock implementation also mentions when the lock will be upgraded to a heavyweight lock (if the CAS operation on the lightweight lock fails, the JVM decides whether to upgrade to a heavyweight lock). Developers need to focus on the inflate(current, obj(), inflate_cause_monitor_enter);
function (the official comments are omitted, and unimportant parts are commented out). The heavyweight lock uses operating system mutexes, which can suspend the waiting threads and release the CPU, making it more suitable for scenarios with high lock contention. The following code completes this inflation operation:
ObjectMonitor* ObjectSynchronizer::inflate(Thread* current, oop object,
const InflateCause cause) {
EventJavaMonitorInflate event;
for (;;) {
const markWord mark = object->mark();
assert(!mark.has_bias_pattern(), "invariant");
// If the Mark Word points to an ObjectMonitor
// * It means the lock of this object has been inflated to a heavyweight lock
// * Therefore, directly return this ObjectMonitor
if (mark.has_monitor()) {
ObjectMonitor* inf = mark.monitor();
markWord dmw = inf->header();
assert(dmw.is_neutral(), "invariant: header=" INTPTR_FORMAT, dmw.value());
return inf;
}
// If the Mark Word is "INFLATING"
// * It indicates another thread is trying to inflate this object's lock
// * So we wait for this operation to complete
if (mark == markWord::INFLATING()) {
read_stable_mark(object);
continue;
}
LogStreamHandle(Trace, monitorinflation) lsh;
// If the Mark Word indicates the object is locked by the current or another thread
if (mark.has_locker()) {
// Create a new ObjectMonitor
ObjectMonitor* m = new ObjectMonitor(object);
// Set the Mark Word to INFLATING
markWord cmp = object->cas_set_mark(markWord::INFLATING(), mark);
if (cmp != mark) {
delete m;
continue;
}
// Store the address of this ObjectMonitor in the Mark Word to complete the inflation
markWord dmw = mark.displaced_mark_helper();
// ......
return m;
}
// If the Mark Word is in the neutral state, it means the object is not locked
assert(mark.is_neutral(), "invariant: header=" INTPTR_FORMAT, mark.value());
// In this case, create a new ObjectMonitor
ObjectMonitor* m = new ObjectMonitor(object);
m->set_header(mark);
// Attempt to store its address in the Mark Word
if (object->cas_set_mark(markWord::encode(m), mark) != mark) {
delete m;
m = NULL;
// If it fails, try again
continue;
}
// Successfully stored, inflation is complete
// ......
return m;
}
}
An important concurrency control mechanism in this process is that only the thread that successfully sets the Mark Word to “INFLATING” can perform the inflation operation. This ensures that no other threads interfere during the inflation process. Additionally, the inflation operation is irreversible; once an object’s lock has been inflated to a heavyweight lock, it cannot be reverted to a lightweight lock.
Lock Revocation
Although the source code for the object header does not include the lock revocation process, lock revocation is also an important part of the synchronized lifecycle. To analyze lock revocation, we need to focus on the exit
function:
void ObjectSynchronizer::exit(oop object, BasicLock* lock, JavaThread* current) {
// First, retrieve the object's markWord
// * This is a data structure that describes the lock state of the object
markWord mark = object->mark();
assert(mark == markWord::INFLATING() ||
!mark.has_bias_pattern(), "should not see bias pattern here");
// Retrieve the displaced header from the BasicLock
// * This structure stores the object's original markWord in the lightweight lock state
markWord dhw = lock->displaced_header();
// If the displaced header value is 0
// * This indicates a recursive lock acquisition
// * That is, the current thread already holds this lock and tries to acquire it again
// * In this case, the exit() function does not need to do any real work
// * Since in a recursive lock, the lock release doesn't need to change the object's state, just return
if (dhw.value() == 0) {
#ifndef PRODUCT
if (mark != markWord::INFLATING()) {
assert(!mark.is_neutral(), "invariant");
assert(!mark.has_locker() ||
current->is_lock_owned((address)mark.locker()), "invariant");
if (mark.has_monitor()) {
ObjectMonitor* m = mark.monitor();
assert(m->object()->mark() == mark, "invariant");
assert(m->is_entered(current), "invariant");
}
}
#endif
return;
}
// If the markWord is the address of BasicLock
// * It means the current thread holds a lightweight lock
// * In this case, exit() tries to use CAS to restore the displaced header to the markWord to release it
// * If this operation succeeds, the function simply returns
if (mark == markWord::from_pointer(lock)) {
assert(dhw.is_neutral(), "invariant");
if (object->cas_set_mark(dhw, mark) == mark) {
return;
}
}
// If none of the above conditions are met
// * It means the lock has been inflated to a heavyweight lock, or is held by another thread
// * In this case, we need to call inflate() to inflate the lightweight lock to a heavyweight lock
// * Then use monitor->exit(current) to release the lock
ObjectMonitor* monitor = inflate(current, object, inflate_cause_vm_internal);
monitor->exit(current);
}
Since the locking process of a heavyweight lock is already very complex, its revocation process must also be quite intricate. The inflate
method has already been analyzed earlier (for heavyweight locks), so here we focus on analyzing the monitor->exit()
function for revoking heavyweight locks, which is implemented in src\hotspot\share\runtime\objectMonitor.cpp
:
void ObjectSynchronizer::exit(oop object, BasicLock* lock, JavaThread* current) {
// First, retrieve the object's markWord
// * This is a data structure that describes the lock state of the object
markWord mark = object->mark();
assert(mark == markWord::INFLATING() ||
!mark.has_bias_pattern(), "should not see bias pattern here");
// Retrieve the displaced header from the BasicLock
// * This structure stores the object's original markWord in the lightweight lock state
markWord dhw = lock->displaced_header();
// If the displaced header value is 0
// * This indicates a recursive lock acquisition
// * That is, the current thread already holds this lock and tries to acquire it again
// * In this case, the exit() function does not need to do any real work
// * Since in a recursive lock, the lock release doesn't need to change the object's state, just return
if (dhw.value() == 0) {
#ifndef PRODUCT
if (mark != markWord::INFLATING()) {
assert(!mark.is_neutral(), "invariant");
assert(!mark.has_locker() ||
current->is_lock_owned((address)mark.locker()), "invariant");
if (mark.has_monitor()) {
ObjectMonitor* m = mark.monitor();
assert(m->object()->mark() == mark, "invariant");
assert(m->is_entered(current), "invariant");
}
}
#endif
return;
}
// If the markWord is the address of BasicLock
// * It means the current thread holds a lightweight lock
// * In this case, exit() tries to use CAS to restore the displaced header to the markWord to release it
// * If this operation succeeds, the function simply returns
if (mark == markWord::from_pointer(lock)) {
assert(dhw.is_neutral(), "invariant");
if (object->cas_set_mark(dhw, mark) == mark) {
return;
}
}
// If none of the above conditions are met
// * It means the lock has been inflated to a heavyweight lock, or is held by another thread
// * In this case, we need to call inflate() to inflate the lightweight lock to a heavyweight lock
// * Then use monitor->exit(current) to release the lock
ObjectMonitor* monitor = inflate(current, object, inflate_cause_vm_internal);
monitor->exit(current);
}
The main goal of this function is to release the lock and ensure that after the lock is released, it is passed to a thread from the waiting thread queue. Throughout the process, multiple memory barriers are used to ensure the order of memory operations. It also handles special cases such as recursive locks and BasicLock ownership.
It is important to note that although heavyweight locks use operating system mutexes, like lightweight locks, they are implemented at the JVM level, not at the OS level.
The entire process begins when the current thread attempts to exit the monitor, then checks the lock ownership, performs recursive lock release or actual lock release, and wakes up waiting threads when necessary. If re-acquiring the lock fails, the responsibility of waking up waiting threads is transferred to the new lock owner, until the heavyweight lock is successfully released.
Performance Optimization
The design and implementation of synchronized
is highly complex and sophisticated, but in high-concurrency scenarios, synchronized
can become a performance bottleneck because it only allows one thread at a time to access methods or code blocks marked as synchronized. This can lead to thread blocking, reducing the throughput of the program.
In business scenarios such as promotions and flash sales, performance requirements are often very high, so synchronized
may not be the best choice. Here are some aspects developers can focus on to optimize performance:
- Reduce Lock Granularity: This can be achieved by locking smaller code blocks instead of entire methods. If only a part of a method needs synchronization, then only that part of the code should be locked, not the entire method.
- Lock Separation: If two unrelated operations need synchronization, use two different locks instead of one. This reduces lock contention and improves performance.
- Avoid Performing Time-Consuming Operations While Holding Locks: Performing time-consuming operations while holding a lock increases the wait time for other threads, which may lead to performance issues.
- Use More Efficient Concurrency Tools: The JUC provides many concurrency tools that are more efficient than
synchronized
, such asReentrantLock
,Semaphore
, andCountDownLatch
. These tools offer more flexible and efficient concurrency control. - Use Lock-Free Data Structures: The Java concurrency library provides some lock-free thread-safe data structures, such as
ConcurrentHashMap
andCopyOnWriteArrayList
. These data structures use optimized concurrency control strategies and are more efficient than those usingsynchronized
. - Use Java 8’s Concurrency API: Java 8 introduced new concurrency APIs, such as
CompletableFuture
, which help better manage concurrent tasks without explicitly using locks.
Reference
[1] Oracle. (2021). The Java® Virtual Machine Specification Java SE 17 Edition. Oracle Corporation. Retrieved May 16, 2023, from https://docs.oracle.com/javase/specs/jvms/se17/html/index.html
[2] Java Magazine Staff. (2020, September 27). A convenient list of essential Java 15 resources. Retrived from https://blogs.oracle.com/javamagazine/post/a-convenient-list-of-essential-java-15-resources
[3] Mateo, P. C. (2021, August 28). JEP 374: Deprecate and Disable Biased Locking. OpenJDK. https://openjdk.java.net/jeps/374
[4] Christian Wimmer. (2008). Synchronization and Object Locking. Retrieved from https://wiki.openjdk.org/display/HotSpot/Synchronization
[5] Michael D. Shah and Samuel Z. Guyer. (2018). Iceberg: dynamic analysis of Java synchronized methods for investigating runtime performance variability. In Companion Proceedings for the ISSTA/ECOOP 2018 Workshops (ISSTA ‘18). Association for Computing Machinery, New York, NY, USA, 119–124. https://doi.org/10.1145/3236454.3236505
[6] Radu Iosif. (2000). Formal verification applied to Java concurrent software. In Proceedings of the 22nd international conference on Software engineering (ICSE ‘00). Association for Computing Machinery, New York, NY, USA, 707–709. https://doi.org/10.1145/337180.337594
[7] Lindholm, T., Yellin, F., Bracha, G., & Buckley, A. (2015). The Java Virtual Machine Specification, Java SE 8 Edition (爱飞翔 & 周志明, Trans.). 机械工业出版社. (Original work published 2015)
[8] Oracle. (2021). The Java® Virtual Machine Specification Java SE 17 Edition. Oracle Corporation. Retrieved May 16, 2023, from https://docs.oracle.com/javase/specs/jvms/se17/html/index.html