Understanding Android ART Native Implementation of synchronized: Monitor, LockWord, and Object
This article explains how the Android Runtime (ART) implements the Java synchronized keyword at the native level, detailing the roles and interactions of Monitor, LockWord, and native Object classes, as well as the thin‑lock to fat‑lock upgrade process and monitor entry/exit mechanisms.
In this article we explore the native side of Android's thread synchronization, focusing on the implementation of the Java synchronized keyword within the ART virtual machine. We start by introducing two important native methods, Monitor::GetContendedMonitor and Monitor::GetLockOwnerThreadId , which expose the current lock and its owning thread.
We then examine the three core classes involved in synchronization: Monitor , Object , and LockWord . Monitor wraps an OS mutex (e.g., futex on Linux) and provides Lock and Unlock operations. Each Object instance contains a monitor_ pointer that actually points to a LockWord , not directly to a Monitor . LockWord is a compact 32‑bit structure that encodes the lock state (unlocked, thin‑locked, fat‑locked, hash code, etc.) and, depending on the state, either stores the owning thread ID or a reference to a Monitor .
The lock state transitions are driven by the MonitorEnter and MonitorExit functions. When a thread attempts to acquire a lock, MonitorEnter reads the current LockWord and, based on its state, either creates a thin lock, increments the recursion count, or inflates the lock to a fat lock. The thin‑lock path stores the thread ID and a recursion counter; if the counter exceeds LockWord::kThinLockMaxCount , the lock is inflated to a fat lock via InflateThinLocked . Fat locks delegate to the underlying OS mutex and may block the thread until the lock becomes available.
ObjPtr<mirror::Object> Monitor::GetContendedMonitor(Thread* thread) {
ObjPtr<mirror::Object> result = thread->GetMonitorEnterObject();
if (result == nullptr) {
MutexLock mu(Thread::Current(), *thread->GetWaitMutex());
Monitor* monitor = thread->GetWaitMonitor();
if (monitor != nullptr) {
result = monitor->GetObject();
}
}
return result;
} uint32_t Monitor::GetLockOwnerThreadId(ObjPtr<mirror::Object> obj) {
DCHECK(obj != nullptr);
LockWord lock_word = obj->GetLockWord(true);
switch (lock_word.GetState()) {
case LockWord::kHashCode:
case LockWord::kUnlocked:
return ThreadList::kInvalidThreadId;
case LockWord::kThinLocked:
return lock_word.ThinLockOwner();
case LockWord::kFatLocked: {
Monitor* mon = lock_word.FatLockMonitor();
return mon->GetOwnerThreadId();
}
default:
LOG(FATAL) << "Unreachable";
UNREACHABLE();
}
}The MonitorEnter algorithm repeatedly reads the LockWord , attempts a compare‑and‑swap (CAS) to install a new thin lock or to inflate to a fat lock, and handles contention by spinning or yielding the CPU before falling back to inflation. The MonitorExit routine mirrors this logic: it validates the lock state, decrements the thin‑lock recursion count, or calls Monitor::Unlock for fat locks.
Inflation is performed by Monitor::Inflate , which creates a new Monitor object, installs it into the Object 's LockWord , and registers the monitor with the runtime's monitor list. The Install method then transfers the thin‑lock information (owner thread ID and recursion count) to the newly created fat lock.
In summary, the ART native layer implements Java synchronization by using a lightweight LockWord to represent thin locks and only allocating a heavyweight Monitor when contention or recursion depth requires it, thereby balancing performance and resource usage.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.