度量代码
到目前为止,我们已经通过3种方式实现了线程安全的单例模式,如下所示。
■ 为所有的操作添加同步机制。
■ 使用双检锁创建单例。
■ 采用线程限定方式(通过ThreadLocal
类)创建单例。
在我们的猜测中,第一种方式的性能应该最差,然而目前我们还没有任何证明数据。现在,我们将创建一个性能基准测试来验证这3种实现方式的性能差异。我们会使用性能测试工具JMH进行性能对比测试,本书后续内容也会多次使用该工具对代码的性能进行测试。
我们会创建一个执行50,000次获取SystemComponent
(单例)对象操作的基准测试(代码请参考代码清单1.8)。我们会创建3个基准测试,每个基准测试使用不同的单例实现方式。为了验证竞争是如何影响程序性能的,我们会创建100个并发线程执行这段代码逻辑。结果报告中以毫秒为单位呈现测试结果。
代码清单1.8 创建单例的基准测试
@Fork(1) @Warmup(iterations = 1) @Measurement(iterations = 1) @BenchmarkMode(Mode.AverageTime) @Threads(100) ◁--- 启动100个并发线程执行这段代码逻辑 @OutputTimeUnit(TimeUnit.MILLISECONDS) public class BenchmarkSingletonVsThreadLocal { private static final int NUMBER_OF_ITERATIONS = 50_000; @Benchmark public void singletonWithSynchronization(Blackhole blackhole) { for (int i = 0; i < NUMBER_OF_ITERATIONS; i++) { blackhole.consume( ➥ SystemComponentSingletonSynchronized.getInstance()); ◁--- 第一个基准测试采用SystemComponentSingletonSynchronized } } @Benchmark public void singletonWithDoubleCheckedLocking(Blackhole blackhole) { for (int i = 0; i < NUMBER_OF_ITERATIONS; i++) { blackhole.consume( ➥ SystemComponentSingletonDoubleCheckedLocking.getInstance()); ◁--- 对SystemComponentSingletonDoubleCheckedLocking的基准测试 } } @Benchmark public void singletonWithThreadLocal(Blackhole blackhole) { for (int i = 0; i < NUMBER_OF_ITERATIONS; i++) { blackhole.consume(SystemComponentThreadLocal.get()); ◁--- 获取SystemComponentThreadLocal的基准测试结果 } } }
执行这个测试,我们可以得到100个并发线程完成50,000次调用的平均耗时。注意,你的实际耗时可能因环境不同有所差异,不过总体的趋势应该保持一致,如代码清单1.9所示。
代码清单1.9 执行不同单例获取的基准测试结果
Benchmark Mode Cnt Score Error Units CH01.BenchmarkSingletonVsThreadLocal.singletonWithDoubleCheckedLocking avgt 2.629 ms/op CH01.BenchmarkSingletonVsThreadLocal.singletonWithSynchronization avgt 316.619 ms/op CH01.BenchmarkSingletonVsThreadLocal.singletonWithThreadLocal avgt 5.622 ms/op
查看测试结果,singletonWithSynchronization
方式的执行的确是最慢的。完成基准测试逻辑执行的平均时间超过300 ms。其他两个方式对这一行为进行了改进。singletonWithDoubleCheckedLockin
g的性能最优,只花费了大约2.6 ms,而singletonWithThreadLocal
耗时大约为5.6 ms。据此,我们可以得出如下结论:采用线程限定方式可以带来约50倍的性能提升,采用双检锁方式可以带来约120倍的性能提升。
验证我们的猜测后,我们为多线程上下文选择了合适的方式。如果需要在多个方式间做选择,当它们的性能不相上下时,我们建议选择更直观的解决方式。然而,所有这一切的前提都是测试数据,如果没有实际的测试数据,我们很难做出客观和理性的决策。
接下来,我们将讨论涉及架构选型的设计取舍。1.3节中,我们会对比微服务架构与单体系统,了解它们在设计上的权衡。