Skip to content

Commit 44aba2e

Browse files
单例模式
1 parent a991c29 commit 44aba2e

File tree

4 files changed

+265
-0
lines changed

4 files changed

+265
-0
lines changed

SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
## 随笔
2929

3030
* [Java Memory Order和Varhandle](jmm.md)
31+
* [基于JVM基建优化的单例模式](lazySingleton.md)
3132

3233
## 翻译
3334

assets/1727018652357-1.png

63.3 KB
Loading

assets/image-20240922232900906.png

119 KB
Loading

lazySingleton.md

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
## 基于JVM优化的单例模式
2+
3+
### 传统单例模式
4+
5+
我们很多时候期望使用全局的惰性单例来实现各种需求,其中出现了经典的基于DCL和类加载机制两种比较常见的模板代码。
6+
7+
一个经典的DCL是这样的
8+
9+
```SQL
10+
public static class DCLStableValue<T> implements StableValue<T> {
11+
public final Supplier<T> factory;
12+
13+
private volatile T cache;
14+
15+
public DCLStableValue(Supplier<T> factory) {
16+
this.factory = factory;
17+
}
18+
19+
20+
@Override
21+
public T get() {
22+
if (cache != null) {
23+
return cache;
24+
}
25+
26+
synchronized (this) {
27+
if (cache != null) {
28+
return cache;
29+
}
30+
return cache = factory.get();
31+
}
32+
}
33+
}
34+
```
35+
36+
而对于类加载机制实现全局的惰性单例则是
37+
38+
```SQL
39+
private static final StableValue<String> classLoad = new StableValue<String>() {
40+
41+
private static class InternalClass {
42+
private static final String lazyValue;
43+
44+
static {
45+
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
46+
lazyValue = UUID.randomUUID().toString();
47+
}
48+
}
49+
50+
@Override
51+
public String get() {
52+
return InternalClass.lazyValue;
53+
}
54+
};
55+
```
56+
57+
其中DCL涉及到大量的volatile读,显然在多线程情况下会有性能损失,而类加载机制虽然最终相当于直接plain访问变量(由类加载器机制保证可见性)但是不够灵活,无法像DCL一样自由选择 `factory`
58+
59+
虽然我们纸面分析了这些优劣,但是还应该通过数据说话,跑个分看下。
60+
61+
```SQL
62+
Benchmark Mode Cnt Score Error Units
63+
StableValueBenchmark.testClassInit thrpt 10 1522698.845 ± 236056.147 ops/s
64+
StableValueBenchmark.testDCL thrpt 10 256534.744 ± 25053.302 ops/s
65+
StableValueBenchmark.testPlain thrpt 10 1531563.936 ± 209274.818 ops/s
66+
```
67+
68+
很明显testClassInit和直接获取一个常量的性能是差不多的,那么有没有综合可以自定义初始化逻辑而且性能也比较好的方案呢?
69+
70+
### invokedynamic
71+
72+
很多写Java的同学听说过invokedynamic这个字节码(下称indy),实际上现在的Java lambda,字符串拼接甚至是模式匹配都是基于这东西做的,它的核心特点在于可以支持惰性绑定调用点而且是线程安全的,只有代码执行到对应的indy的字节码时再去调用一个特殊的方法(下称bootstrapMethod,BSM)来指定一个CallSite作为真实链接到的方法,而这个BSM的具体实现是可以自定义的,所以就给了我们很大的操作空间,下方是一个很简单的BSM实现。
73+
74+
```SQL
75+
public static ConstantCallSite indyFactory(MethodHandles.Lookup lookup, String name, MethodType type, Object... args) throws NoSuchFieldException, IllegalAccessException {
76+
Class<?> aClass = lookup.lookupClass();
77+
Supplier supplier = MethodHandles.classData(lookup, DEFAULT_NAME, Supplier.class)
78+
return new ConstantCallSite(MethodHandles.constant(type.returnType(), supplier.get()));
79+
}
80+
```
81+
82+
MethodHandles.*constant* 实际上是一个调用会返回一个常量值的Methodhandle,如果你不太了解什么是Methodhandle,那么你可以简单的把它理解成Java的函数指针,它本质上就是描述一个Java方法是怎么被JVM链接并且调用的。
83+
84+
> 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.
85+
86+
ConstantCallSite这个作为CallSite的一个子类有个好处在于可以在调用点被JIT直接内联展开,直接原地访问。然后我们就可以利用字节码工具(这里使用的是Java21引入的ClassFile API),动态生成一个接口的实现,在对应的方法里面填入indy字节码,使其引导到我们这个BSM上。
87+
88+
那么其实这里还有个新东西`MethodHandles::classData`这是什么?这个就是一个动态常量,是一个跟java.lang.class相绑定的东西,当我们在运行时通过Lookup定义一个类的时候就可以通过传入这个东西,隐式传递一个运行时引用
89+
90+
再通过`MethodHandles::classData`获取出来,具体可以参考这个https://bugs.openjdk.org/browse/JDK-8256214?attachmentViewMode=list
91+
92+
![image-20240922232900906](assets/image-20240922232900906.png)
93+
94+
那么这段字节码该如何写呢?
95+
96+
```java
97+
byte[] classByteCode = classFile.build(ClassDesc.of(className), cb -> {
98+
cb.withInterfaceSymbols(ClassDesc.of(StableValue.class.getName()));
99+
cb.withMethodBody(ConstantDescs.INIT_NAME, ConstantDescs.MTD_void, AccessFlags.ofMethod(AccessFlag.PUBLIC).flagsMask(), it -> {
100+
it.aload(0);
101+
it.invokespecial(CD_Object, INIT_NAME, MTD_void);
102+
it.return_();
103+
});
104+
cb.withMethodBody(
105+
"get",
106+
MethodTypeDesc.of(ClassDesc.of(Object.class.getName())),
107+
AccessFlags.ofMethod(AccessFlag.PUBLIC, AccessFlag.SYNTHETIC).flagsMask(),
108+
it -> {
109+
//这里就是invokedynamic字节码
110+
it.invokeDynamicInstruction(
111+
DynamicCallSiteDesc.of(
112+
MethodHandleDesc.ofMethod(
113+
DirectMethodHandleDesc.Kind.STATIC, StableValueGenerator.class.describeConstable().get(), "indyFactory",
114+
INDY_MTD
115+
),
116+
"get",
117+
MethodType.methodType(Object.class).describeConstable().get()
118+
)
119+
);
120+
it.returnInstruction(TypeKind.from(Object.class));
121+
}
122+
);
123+
});
124+
MethodHandles.Lookup lookup = MethodHandles.lookup();
125+
//在这里定义隐藏类和初始化classdata
126+
Class<?> aClass = lookup.defineHiddenClassWithClassData(classByteCode, factory, false).lookupClass()
127+
128+
return ((StableValue) aClass.newInstance());
129+
```
130+
131+
对应产物反编译就是这样的:
132+
133+
```Java
134+
// class version 66.0 (66)
135+
// access flags 0x1
136+
public class io/github/dreamlike/stableValue/StableValueImpl0 implements io/github/dreamlike/stableValue/StableValue {
137+
138+
139+
// access flags 0x1
140+
public <init>()V
141+
ALOAD 0
142+
INVOKESPECIAL java/lang/Object.<init> ()V
143+
RETURN
144+
MAXSTACK = 1
145+
MAXLOCALS = 1
146+
147+
// access flags 0x1001
148+
public synthetic get()Ljava/lang/Object;
149+
INVOKEDYNAMIC get()Ljava/lang/Object; [
150+
// handle kind 0x6 : INVOKESTATIC
151+
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;
152+
]
153+
ARETURN
154+
MAXSTACK = 1
155+
MAXLOCALS = 1
156+
}
157+
```
158+
159+
160+
161+
而且用起来也很简单,其实除了ClassFile API之外都是Java8可用的,换个字节码库就能运行在Java8上得到一个通用的高性能惰性单例工具
162+
163+
```SQL
164+
private static final StableValue<String> valueFinal = StableValue.of(() -> {
165+
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
166+
return UUID.randomUUID().toString();
167+
});
168+
```
169+
170+
### ConstantDynamic
171+
172+
ConstantDynamic是一个特殊的字节码,其储存在常量池中,当被load到栈顶时会执行BootstrapMethod,然后被jit将返回值缓存起来,之后再次调用时直接返回缓存的值,这个特性正好可以用于实现高性能的懒加载单例。
173+
174+
>ConstantDynamic是什么具体可以参考[JEP 309: Dynamic Class-File Constants](https://openjdk.org/jeps/309) 以及 [hands-on-java-constantdynamic](https://www.javacodegeeks.com/2018/08/hands-on-java-constantdynamic.html)
175+
176+
它对应的BSM其实跟invokedynamic的BSM很像,只不过把MethodType换成了对位的Class
177+
178+
```java
179+
public static Object condyFactory(MethodHandles.Lookup lookup, String name, Class type) throws NoSuchFieldException, IllegalAccessException {
180+
Class<?> aClass = lookup.lookupClass();
181+
Supplier supplier = MethodHandles.classData(lookup, DEFAULT_NAME, Supplier.class);
182+
return supplier.get();
183+
}
184+
```
185+
186+
对应的字节码写起来更简单了
187+
188+
```java
189+
byte[] classByteCode = classFile.build(ClassDesc.of(className), cb -> {
190+
cb.withInterfaceSymbols(ClassDesc.of(StableValue.class.getName()));
191+
cb.withMethodBody(ConstantDescs.INIT_NAME, ConstantDescs.MTD_void, AccessFlags.ofMethod(AccessFlag.PUBLIC).flagsMask(), it -> {
192+
it.aload(0);
193+
it.invokespecial(CD_Object, INIT_NAME, MTD_void);
194+
it.return_();
195+
});
196+
cb.withMethodBody(
197+
"get",
198+
MethodTypeDesc.of(ClassDesc.of(Object.class.getName())),
199+
AccessFlags.ofMethod(AccessFlag.PUBLIC, AccessFlag.SYNTHETIC).flagsMask(),
200+
it -> {
201+
it.constantInstruction(
202+
DynamicConstantDesc.of(
203+
ConstantDescs.ofConstantBootstrap(StableValueGenerator.class.describeConstable().get(), "condyFactory", Object.class.describeConstable().get())
204+
));
205+
it.returnInstruction(TypeKind.from(Object.class));
206+
}
207+
);
208+
});
209+
//define和classdata部分与invokedynamic实现一致
210+
```
211+
212+
生成出来的字节码(节选)
213+
214+
```java
215+
public io.github.dreamlike.stableValue.StableValueImpl2();
216+
descriptor: ()V
217+
flags: (0x0001) ACC_PUBLIC
218+
Code:
219+
stack=1, locals=1, args_size=1
220+
0: aload_0
221+
1: invokespecial #8 // Method java/lang/Object."<init>":()V
222+
4: return
223+
224+
public java.lang.Object get();
225+
descriptor: ()Ljava/lang/Object;
226+
flags: (0x1001) ACC_PUBLIC, ACC_SYNTHETIC
227+
Code:
228+
stack=1, locals=1, args_size=1
229+
0: ldc #21 // Dynamic #0:_:Ljava/lang/Object;
230+
2: areturn
231+
}
232+
BootstrapMethods:
233+
0: #17 REF_invokeStatic io/github/dreamlike/stableValue/StableValueGenerator.condyFactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;
234+
Method arguments:
235+
```
236+
237+
### 性能
238+
239+
可以直接参考https://github.com/dreamlike-ocean/StableValue的实现
240+
241+
```
242+
并发线程数目为5
243+
Benchmark Mode Cnt Score Error Units
244+
stableValue.Benchmark.StableValueBenchmarkCase.testClassInit thrpt 10 1998369.352 ± 32731.935 ops/s
245+
stableValue.Benchmark.StableValueBenchmarkCase.testDCL thrpt 10 472392.857 ± 10495.215 ops/s
246+
stableValue.Benchmark.StableValueBenchmarkCase.testIndyStabValue thrpt 10 1996429.795 ± 32976.993 ops/s
247+
stableValue.Benchmark.StableValueBenchmarkCase.testIndyStabValueCody thrpt 10 1998819.221 ± 29030.771 ops/s
248+
stableValue.Benchmark.StableValueBenchmarkCase.testIndyStabValueHidden thrpt 10 2006979.049 ± 21622.126 ops/s
249+
stableValue.Benchmark.StableValueBenchmarkCase.testPlain thrpt 10 2015192.566 ± 16183.806 ops/s
250+
251+
```
252+
253+
254+
255+
###
256+
257+
其实写了这么多,都是为了铺垫这样一个Java新特性——StableValue,一种更方便且性能更好的的惰性初始化,让@Stable这个注解更适合开发者使用
258+
259+
https://openjdk.org/jeps/8312611
260+
261+
期待这个特性合入master,pr可参考https://github.com/openjdk/jdk/pull/19625
262+
263+
###
264+

0 commit comments

Comments
 (0)