软件开发中的决策:权衡与取舍
上QQ阅读APP看书,第一时间看更新

度量代码

到目前为止,我们已经通过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。其他两个方式对这一行为进行了改进。singletonWithDoubleCheckedLocking的性能最优,只花费了大约2.6 ms,而singletonWithThreadLocal耗时大约为5.6 ms。据此,我们可以得出如下结论:采用线程限定方式可以带来约50倍的性能提升,采用双检锁方式可以带来约120倍的性能提升。

验证我们的猜测后,我们为多线程上下文选择了合适的方式。如果需要在多个方式间做选择,当它们的性能不相上下时,我们建议选择更直观的解决方式。然而,所有这一切的前提都是测试数据,如果没有实际的测试数据,我们很难做出客观和理性的决策。

接下来,我们将讨论涉及架构选型的设计取舍。1.3节中,我们会对比微服务架构与单体系统,了解它们在设计上的权衡。