回看历史
我们很多时候期望使用全局的惰性单例来实现各种需求,其中出现了经典的基于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
}
具体的代码实现可以参考
而且用起来也很简单,其实除了ClassFile API之外都是Java8可用的,换个字节码库就能运行在Java8上得到一个通用的高性能惰性单例工具
private static final StableValue<String> valueFinal = StableValue.of(() -> {
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
return UUID.randomUUID().toString();
});
最后我们看看跑分 看起来还不错?
醋
其实写了这么多,都是为了铺垫这样一个Java新特性——StableValue,一种更方便且性能更好的的惰性初始化,让@Stable这个注解更适合开发者使用
期待这个特性合入master,pr可参考github.com/openjdk/jdk…