Java是否曾经偏重单个锁

Mar*_* VY 3 multithreading jvm synchronized jvm-hotspot biased-locking

这个问题是关于偏向锁定的启发式Java使用之一。下一段是给将来的读者的。我怀疑任何可以回答这个问题的人都可以安全地跳过它。

据我了解,曾几何时,人们注意到Java有许多线程安全的类,但是它们的实例往往仅由一个线程使用,因此Sun引入了偏向锁以利用这一点。问题是,如果您“猜错了”并尝试从两个线程中偏向需要使用的锁,则即使没有争用,也需要撤消(“撤销”)偏见,这太昂贵了,以至于JVM会尽力避免这种情况,即使这意味着有时在偏向锁定可能会最终获胜的情况下也会错过这一机会。

我也知道有时JVM会决定执行“批量”重新偏置,并将许多某种类型的所有锁迁移到另一个线程。这个问题不是关于这个的。出于这个问题的目的,假设我只有两个线程和一个锁。(实际情况更加复杂,并且涉及线程池,但现在暂时不考虑它。确实,假装我没有提到它。)进一步假设线程A沿着“睡眠几秒钟”的方式运行无限循环。 ,在锁定下递增整数,然后重复”。(并不是真的没什么用,但这应该足以使观点明白。)同时,线程B运行类似的循环,但是睡眠时间是几小时而不是几秒钟。进一步假设调度程序是不可思议的,并保证永远不存在任何争用。

现在,假设我们关心线程A唤醒和成功递增其整数之间的平均延迟。据我了解,JVM最初会将锁定偏向A,然后在线程B第一次唤醒时取消偏向。

我的问题是:JVM是否会认识到其最初的猜测基本上是正确的,从而再次将锁重新分配给线程A?

Iva*_*tov 6

从理论上讲是可能的,但需要一些其他条件和特殊的JVM设置。

理论

在某些对象上,偏向锁显然是无益的,例如涉及两个或多个线程的生产者-消费者队列。这样的对象必然具有锁争用。另一方面,在某些情况下,将一组对象重新偏置到另一个线程的能力是有利的,特别是当一个线程分配了许多对象并对每个对象执行初始同步操作,而另一个线程对此执行后续工作时,尤其如此示例基于弹簧的应用程序。

JVM尝试涵盖这两种用例,并同时支持重定序和撤销。请参阅“通过偏置锁定和批量重新偏置消除与同步相关的原子操作”中的详细说明。

换句话说,您的理解是:

据我了解,JVM最初会将锁定偏向A,然后在线程B第一次唤醒时取消偏向。

并非总是如此,即JVM足够聪明,可以检测到无竞争的同步并向另一个线程重新锁定该锁。

以下是一些实施说明:

  • HotSpot仅支持批量重新偏置,以分摊每对象偏差撤销的成本,同时保留优化的好处。
  • 批量重新偏置和批量撤消共享一个安全点/操作名称-RevokeBias。这非常令人困惑,需要其他调查。
  • 当且仅当撤销数量大于BiasedLockingBulkRebiasThreshold和小于BiasedLockingBulkRevokeThreshold且最近的撤销不晚于时BiasedLockingDecayTime,才可能进行后续的批量重偏置,其中所有转义的变量均为JVM属性。请仔细阅读此代码
  • 您可以使用property跟踪安全点事件-XX:+PrintSafepointStatistics。最有趣的是EnableBiasedLocking,RevokeBias和BulkRevokeBias
  • -XX:+TraceBiasedLocking 生成一个有趣的日志,其中包含有关JVM决策的详细描述。

实践

这是我的复制器,其中一个线程(实际上是主线程)分配监视对象并对其执行初始同步操作,然后另一个线程执行后续工作:

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
    }

}
Run Code Online (Sandbox Code Playgroud)

为了重现我的结果,请使用以下JVM参数:

  • -XX:+ UseBiasedLocking-默认情况下不需要
  • -XX:BiasedLockingStartupDelay = 0-默认情况下延迟4秒
  • -XX:+ PrintSafepointStatistics -XX:PrintSafepointStatisticsCount = 1-启用安全点日志
  • -XX:+ TraceBiasedLocking-非常有用的日志
  • -XX:BiasedLockingBulkRebiasThreshold = 1-在我的示例中减少迭代次数

在测试过程中,JVM决定重新监控监视器而不是撤销

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
Run Code Online (Sandbox Code Playgroud)

下一步是将锁偏向主线程。这部分是最难的部分,因为我们必须遵循以下试探法

  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;
  }
Run Code Online (Sandbox Code Playgroud)

您可以使用JVM参数和我的示例来尝试这种启发式方法,但请记住,这非常困难,有时需要进行JVM调试。