Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does Java ever rebias an individual lock

This question is about the one of the heuristics Java uses of biased locking. The next paragraph is for future readers; I suspect anyone who can answer this question can safely skip it.

As far as I understand, once upon a time, people noticed that Java has lots of classes that are thread safe, but whose instances tend to be used by only one thread, so Sun introduced biased locking to take advantage of that. The trouble is, if you "guess wrong" and try to bias a lock that needs to be used from two threads, the bias needs to be undone ("revoked") even if there is no contention, and this is so expensive that the JVM tries hard to avoid it, even if it means sometimes missing out on situations where biased locking could have been a net win.

I also know that sometimes the JVM decides the do a "bulk" re-bias, and migrate many all locks of a certain type to a different thread. This question is NOT about that. For the purpose of this question, suppose I only have two threads and a single lock. (The real situation is more complicated and involves thread pools, but let's ignore that for now. Really, pretend I didn't mention it.) Suppose further that Thread A runs an infinite loop something along the lines of "sleep for a few seconds, increment integer under lock, repeat". (It's not really that useless, but this should be enough to get the point across.) Meanwhile, Thread B runs a similar loop, but the sleep time is several hours instead of a few seconds. Suppose further that the scheduler is magical and guarantees that there is never any contention. (Preemptive nitpicking: we could just a volatile if that were true. This is just an example. Work with me here.) This assumption is unrealistic, but I'm trying to worry about just one thing at a time.

Now, suppose we care about the average latency between thread A waking up and successfully incrementing its integer. 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.

My question is: would the JVM ever realize that its initial guess was basically correct, and thus re-bias the lock towards Thread A again?

like image 792
Mark VY Avatar asked Jan 30 '23 13:01

Mark VY


1 Answers

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.

like image 175
Ivan Mamontov Avatar answered Feb 02 '23 10:02

Ivan Mamontov