JEP draft: Race exclusion for confined data

OwnerJohn Rose
TypeFeature
ScopeJDK
StatusDraft
Componenthotspot / runtime
EffortL
DurationL
Created2017/05/19 07:49
Updated2020/02/27 22:54
Issue8180647

(DRAFT DRAFT DRAFT)

Summary

Java's synchronization mechanism requires that there is a monitor in the header of every object on the heap (including Java arrays). The monitor is used by only a small fraction of all objects. The semantics, as fixed by the JVMS and JLS, are too inflexible for some use cases, and for many others the extra header word is just wasted space. For still other use cases, the monitor is too weak, since the JMM allows racing memory accesses to defeat synchronization.

We propose to make more flexible use of the per-object monitor and, in a class-specific way, allow it to be either expanded or eliminated, to support a wider range of use cases. In particular, we will support the use cases of single-thread confinement, permanent immutability, and episodic immutability, by providing a "hook" to exclude racing accesses to appropriately configured objects.

Description

An object which is under synchronization is owned by a thread, but its public non-final fields (and, if an array, its elements) are still subject to racing writes.

Likewise, if an object is serving as a handle to off-heap memory resources (like a DirectByteBuffer), it may prevent racing access to itself by ensuring single-threaded access, but it cannot prevent racing accesses to its off-heap storage without unusual measures such as locking a monitor on every access, measures which are often rejected as too expensive. The possibility of racing accesses to deallocated memory prevents us from creating a safe API for deallocating off-heap memory resources, which is a serious and recurrent problem for many customers working with memory intensive applications.

We need a new kind of locking, with the following characteristics:

Contention for a variable can be handled in two ways: Blocking or throwing. The existing Java sychronization supports blocking, which means that contention must be resolved actively among the current owner and a pool of waiting threads, and this must be done at every release of a lock. This makes both lock acquisition and lock release very expensive to implement, requiring inter-processor communication (via fenced or volatile memory accesses).

Throwing, on the other hand, immediately eliminates contending threads. It forces the programmer to think about the top-level contention structure of the program, and deploy a smaller number of explicitly coded blocking monitors to manage the exclusion needs of all the objects in the program.

When managing contention with throwing, the monitor-enter operation simply consists of verifying that the object header has an expected bit pattern, corresponding to the current thread. (If the object is not frozen, a read access must check for that bit pattern also.) There is nothing to do for monitor-exit (1+0).

Since the object header cannot change from a valid state to an invalid state without the current thread making the change, the lock check can easily be commoned up and hoisted out of loops, just like null checks or range checks.

(This property of hoistability is a very strong one, allowing cheap but safe data access, but also strongly constraining the allowable lock state transitions. It seems likely that once an object is made immutable its lock state can never change again.)

Hoistable lock checking is cheap enough to justify creating the new class of lockable fields, for which every access is preceded by a throwing contention check. Of course, some users will still prefer "naked" mutable fields, but the requirement to perform defensive copy of racy objects will in some cases turn out to be more burdensome than allowing the JVM to exclude races by managing field locks.

Biased locking recalled: At one point it seemed to many that the technique of biased locking could render Java synchronization cheap enough to use freely. What we learned from that adventure was that objects which are stably "owned" by one thread can indeed be easily verified by all parties to be in that state. We also learned that lock revocation can be so expensive as to make the whole scheme impractical. Lock revocation must occur when an object X is biased to a thread T1 that no longer cares about it, and another thread T2 is trying to get access. To grant the lock to T2, T1 must be interrupted and brought to a provably safe state relative to X. This in general requires walking the stack of T1 to discover any lingering references to X that might logically be synchronized to T1. The problem here is not with biased ownership, but with the user model in which the hand-off from T1 to T2 is implicit, and can occur between any two episodes of synchronization by T1. The new form of object locking fixes the problems with biased locking while retaining the benefits.

Field locking interoperates with classic Java synchronization. When a new thread enters the monitor of an object (monitor-enter), it requests a handoff from old owner of the object. When the old owner grants the request, the new thread becomes the object's owner, and all lockable fields are exclusively available to the new thread. When the monitor is exited (monitor-exit) the ownership is handed back to the old owner (if it was a terminal) or perhaps some other state determined by the object's class. During the critical section, the owning thread can (and should) nominate the successor lock state for the locked object. This state is chosen from:

With respect to lock states, the monitor-exit operation is similar to a return from a constructor. In both cases, there is a controlled handoff from the current thread (which is either constructing a new object or modifying an old one) to the next owner.

A standard terminal will be supplied which has no specific properties but will grant access to any thread. Locking an object to this terminal has the effect of temporarily disabling all reads or writes, until a thread picks up the object and does something else with it.