基于invokdynamic实现单例模式的性能提升

245 阅读3分钟

回看历史

我们很多时候期望使用全局的惰性单例来实现各种需求,其中出现了经典的基于DCL和类加载机制两种比较常见的模板代码。

一个经典的DCL是这样的

public static class DCLStableValue<T> implements StableValue<T> {
    public final Supplier<T> factory;

    private volatile T cache;

    public DCLStableValue(Supplier<T> factory) {
        this.factory = factory;
    }


    @Override
    public T get() {
        if (cache != null) {
            return cache;
        }

        synchronized (this) {
            if (cache != null) {
                return cache;
            }
            return cache = factory.get();
        }
    }
}

而对于类加载机制实现全局的惰性单例则是

private static final StableValue<String> classLoad = new StableValue<String>() {

    private static class InternalClass {
        private static final String lazyValue;

        static {
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
            lazyValue = UUID.randomUUID().toString();
        }
    }

    @Override
    public String get() {
        return InternalClass.lazyValue;
    }
};

其中DCL涉及到大量的volatile读,显然在多线程情况下会有性能损失,而类加载机制虽然最终相当于直接plain访问变量(由类加载器机制保证可见性)但是不够灵活,无法像DCL一样自由选择 factory 。

虽然我们纸面分析了这些优劣,但是还应该通过数据说话,跑个分看下。

Benchmark                                     Mode  Cnt        Score        Error  Units
StableValueBenchmark.testClassInit           thrpt   10  1522698.845 ± 236056.147  ops/s
StableValueBenchmark.testDCL                 thrpt   10   256534.744 ±  25053.302  ops/s
StableValueBenchmark.testPlain               thrpt   10  1531563.936 ± 209274.818  ops/s

很明显testClassInit和直接获取一个常量的性能是差不多的,那么有没有综合可以自定义初始化逻辑而且性能也比较好的方案呢?

invokedynamic

很多写Java的同学听说过invokedynamic这个字节码(下称indy),实际上现在的Java lambda,字符串拼接甚至是模式匹配都是基于这东西做的,它的核心特点在于可以支持惰性绑定调用点而且是线程安全的,只有代码执行到对应的indy的字节码时再去调用一个特殊的方法(下称bootstrapMethod,BM)来指定一个CallSite作为真实链接到的方法,而这个BM的具体实现是可以自定义的,所以就给了我们很大的操作空间,下方是一个很简单的BM实现。

public static ConstantCallSite indyFactory(MethodHandles.Lookup lookup, String name, MethodType type, Object... args) {
    String key = (String) args[0];
    Supplier<Object> supplier = factories.get(key);
    if (supplier == null) {
        throw new IllegalArgumentException("No factory found for key: " + key);
    }
    return new ConstantCallSite(MethodHandles.constant(type.returnType(), supplier.get()));
}

MethodHandles.constant 实际上是一个调用会返回一个常量值的Methodhandle,如果你不太了解什么是Methodhandle,那么你可以简单的把它理解成Java的函数指针,它本质上就是描述一个Java方法是怎么被JVM链接并且调用的。

紧接着就是ConstantCallSite是什么? 这里贴一段JDK里面对于这个 ConstantCallSite 的描述:A ConstantCallSite is a CallSite whose target is permanent, and can never be changed. An invokedynamic instruction linked to a ConstantCallSite is permanently bound to the call site's target.

ConstantCallSite这个作为CallSite的一个子类有个好处在于可以在调用点被JIT直接内联展开,直接原地访问。然后我们就可以利用字节码工具(这里使用的是Java21引入的ClassFile API),动态生成一个接口的实现,在对应的方法里面填入indy字节码,使其引导到我们这个BM上

// class version 66.0 (66)
// access flags 0x1
public class io/github/dreamlike/stableValue/StableValueImpl0 implements io/github/dreamlike/stableValue/StableValue {


  // access flags 0x1
  public <init>()V
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1001
  public synthetic get()Ljava/lang/Object;
    INVOKEDYNAMIC get()Ljava/lang/Object; [
      // handle kind 0x6 : INVOKESTATIC
      io/github/dreamlike/stableValue/StableValueGenerator.indyFactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/ConstantCallSite;
      // arguments:
      "io.github.dreamlike.stableValue.StableValueImpl0"
    ]
    ARETURN
    MAXSTACK = 1
    MAXLOCALS = 1
}

具体的代码实现可以参考

github.com/dreamlike-o…

而且用起来也很简单,其实除了ClassFile API之外都是Java8可用的,换个字节码库就能运行在Java8上得到一个通用的高性能惰性单例工具

private static final StableValue<String> valueFinal = StableValue.of(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    return UUID.randomUUID().toString();
});

最后我们看看跑分 看起来还不错?

image

其实写了这么多,都是为了铺垫这样一个Java新特性——StableValue,一种更方便且性能更好的的惰性初始化,让@Stable这个注解更适合开发者使用

期待这个特性合入master,pr可参考github.com/openjdk/jdk…

附录:跑分参数

jmh参数

image

CPU

image

JVM和OS

image