In theory it is possible but requires a few additional conditions and special JVM settings.
Theory
There are certain objects for which biased locking is obviously unprofitable, such as producer-consumer queues where two or more threads are involved. Such objects necessarily have lock contention. On the other hand there are
situations in which the ability to rebias a set of objects to another thread is profitable, in particular when one thread allocates many
objects and performs an initial synchronization operation on each,
but another thread performs subsequent work on them, for example spring based applications.
JVM tries to cover both use cases and supports rebaising and revocation at the same time. See detailed explanation in Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing
In other words your understanding:
As far as I understand, the JVM would initially bias the lock towards
A, and then revoke the bias the first time that thread B woke up.
is not always true i.e. JVM is smart enough to detect uncontended synchronization and rebias the lock towards to another thread.
Here is some implementation notes:
- HotSpot supports only bulk rebiasing to amortize the cost of per-object bias revocation while retaining the benefits of the optimization.
- bulk rebias and bulk revoke share one safepoint/operation name - RevokeBias. This is very confusing and requires additional investigations.
- subsequent bulk rebias is possible if and only if number of revokes more than
BiasedLockingBulkRebiasThreshold
and less than BiasedLockingBulkRevokeThreshold
and the latest revoke was not later than BiasedLockingDecayTime
, where all escaped variables are JVM properties. Please read carefully this code.
- you can trace safepoint events with property
-XX:+PrintSafepointStatistics
. The most interesting are EnableBiasedLocking, RevokeBias and BulkRevokeBias
-XX:+TraceBiasedLocking
produces an interesting log with detailed descriptions about JVM decisions.
Practice
Here is my reproducer, where one thread(actually main thread) allocates monitor object and performs an initial synchronization operation on it, then another thread performs subsequent work:
package samples;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.lang.System.out;
public class BiasLocking {
private static final Unsafe U;
private static final long OFFSET = 0L;
static {
try {
Field unsafe = Unsafe.class.getDeclaredField("theUnsafe");
unsafe.setAccessible(true);
U = (Unsafe) unsafe.get(null);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
public static void main(String[] args) throws Exception {
ExecutorService thread = Executors.newSingleThreadExecutor();
for (int i = 0; i < 15; i++) {
final Monitor a = new Monitor();
synchronized (a) {
out.println("Main thread \t\t" + printHeader(a));
}
thread.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
synchronized (a) {
out.println("Work thread \t\t" + printHeader(a));
}
return null;
}
}).get();
}
thread.shutdown();
}
private static String printHeader(Object a) {
int word = U.getInt(a, OFFSET);
return Integer.toHexString(word);
}
private static class Monitor {
// mutex object
}
}
In order to reproduce my results please use the following JVM arguments:
- -XX:+UseBiasedLocking - is not required is used by default
- -XX:BiasedLockingStartupDelay=0 - by default there is a delay 4s
- -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 - to enable safepoint log
- -XX:+TraceBiasedLocking - very useful log
- -XX:BiasedLockingBulkRebiasThreshold=1 - to reduce amount of iterations in my example
In the middle of test JVM decides to rebaise monitor instead of revocation
Main thread 0x7f5af4008805 <-- this is object's header word contains thread id
* Beginning bulk revocation (kind == rebias) because of object 0x00000000d75631d0 , mark 0x00007f5af4008805 , type samples.BiasLocking$Monitor
* Ending bulk revocation
Rebiased object toward thread 0x00007f5af415d800
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
0.316: BulkRevokeBias [ 10 0 0 ] [ 0 0 0 0 0 ] 0
Work thread 0x7f5af415d905 <-- this is object's header word contains thread id => biased
The next step is to rebias the lock towards to the main thread. This part is the hardest one because we have to hit the following heuristics:
Klass* k = o->klass();
jlong cur_time = os::javaTimeMillis();
jlong last_bulk_revocation_time = k->last_biased_lock_bulk_revocation_time();
int revocation_count = k->biased_lock_revocation_count();
if ((revocation_count >= BiasedLockingBulkRebiasThreshold) &&
(revocation_count < BiasedLockingBulkRevokeThreshold) &&
(last_bulk_revocation_time != 0) &&
(cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) {
// This is the first revocation we've seen in a while of an
// object of this type since the last time we performed a bulk
// rebiasing operation. The application is allocating objects in
// bulk which are biased toward a thread and then handing them
// off to another thread. We can cope with this allocation
// pattern via the bulk rebiasing mechanism so we reset the
// klass's revocation count rather than allow it to increase
// monotonically. If we see the need to perform another bulk
// rebias operation later, we will, and if subsequently we see
// many more revocation operations in a short period of time we
// will completely disable biasing for this type.
k->set_biased_lock_revocation_count(0);
revocation_count = 0;
}
You can play with JVM parameters and my example to hit this heuristics, but keep in mind, it is very hard and sometimes requires a JVM debugging.