字符串拼接-容易被忽视的代码性能陷阱

本文主旨

通过JMH(一个牛逼的基准测试工具),从0开始使用,探索关于字符串拼接几种方式的性能问题。

一、使用 JMH 探索字符串拼接

第一步:生成一个新的JMH工程的maven命令如下:

mvn archetype:generate -DinteractiveMode=false 
-DarchetypeGroupId=org.openjdk.jmh
-DarchetypeArtifactId=jmh-java-benchmark-archetype 
-DgroupId=com.bin.jmh 
-DartifactId=jmh 
-Dversion=1.0.0-SNAPSHOT 

生成的工程如下:
在这里插入图片描述
第二步:添加几种方式的字符串拼接测试代码

方式一、String

@Warmup(iterations = 5, time = 1)  //预热 迭代5次,每次一秒
@Measurement(iterations = 5, time = 1) //正式测,迭代5次,每次一秒
@Fork(1)  //几个线程执行   
@BenchmarkMode(Mode.Throughput) //基准测试的模式 吞吐量
@OutputTimeUnit(TimeUnit.SECONDS) //时间单位为秒
public class MyBenchmark {

    @Benchmark
    public String testMethod() {

        String targetString = "";
        for (int i = 0; i < 10000; i++) {
           targetString += "helllo";
        }
        return targetString;
    }
}

第三步:打包、运行

  1. mvn clean install
  2. java -jar target/benchmarks.jar

对应的输出结果如下:

# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Benchmark mode: Throughput, ops/time
# Benchmark: com.afei.jmh.MyBenchmark.testMethod

# Run progress: 0.00% complete, ETA 00:00:10
# Fork: 1 of 1
# Warmup Iteration   1: 3.718 ops/s
# Warmup Iteration   2: 4.691 ops/s
# Warmup Iteration   3: 22.840 ops/s
# Warmup Iteration   4: 26.564 ops/s
# Warmup Iteration   5: 25.708 ops/s
Iteration   1: 25.399 ops/s
Iteration   2: 26.057 ops/s
Iteration   3: 26.828 ops/s
Iteration   4: 25.474 ops/s
Iteration   5: 24.801 ops/s

Result "testMethod":
  25.712 ±(99.9%) 2.951 ops/s [Average]
  (min, avg, max) = (24.801, 25.712, 26.828), stdev = 0.766
  CI (99.9%): [22.761, 28.663] (assumes normal distribution)

# Run complete. Total time: 00:00:10

Benchmark                Mode  Cnt   Score   Error  Units
MyBenchmark.testMethod  thrpt    5  25.712 ± 2.951  ops/s

我们只需要关注"Score"这一栏,它表示的是每秒执行的基准测试方法的次数。

对于String字符串,拼接1万次。一秒钟可以执行25.712次,Error 表示上下浮动,可以忽略只关心整数部分即可。

方式二、StringBuilder

替换测试代码,使用StringBuffer进行字符串拼接

  @Benchmark
    public String testMethod() {

        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            builder.append("hello");
        }

        return builder.toString();
    }

打包、执行,结果如下:

Benchmark                Mode  Cnt      Score      Error  Units
MyBenchmark.testMethod  thrpt    5  13209.264 ± 1136.107  ops/s

我们震惊的发现,同样的逻辑,使用StringBuilder每秒可以执行13209次。

方式三、StringBuffer

替换为StringBuffer测试

    @Benchmark
    public String testMethod() {

        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < 10000; i++) {
            buffer.append("hello");
        }

        return buffer.toString();
    }

结果如下:

Benchmark                Mode  Cnt      Score      Error  Units
MyBenchmark.testMethod  thrpt    5  13621.655 ± 3155.081  ops/s

同样的逻辑,使用StringBuffer每秒可以执行13621次。

基准测试结论:使用 StringBuilder 的字符串拼接操作是String操作的 528 倍;使用 StringBuffer 的字符串拼接操作,是String的544倍。因此,频繁的字符串拼接不建议使用String,比如:打印日志,使用String会导致频繁的full gc。

二、思考

1、 String 的字符串连接操作为什么慢呢?

这是因为String是不可变类,每一次字符串拼接(targetString += “hello”),都需要创建一个新的 String 对象,这种模式对CPU 和内存消耗都比较大。

2、 StringBuilder 和 StringBuffer 的为什么快?

因为 StringBuilder 和 StringBuffer 是可变类,内部预先分配了一定的内存。字符串操作时,只有当预分配内存不足,才会扩展内存,大幅度减少了内存分配、拷贝和释放的频率。

3、 StringBuilder 和 StringBuffer 的性能对比?

StringBuffer 的字符串操作是多线程安全的, StringBuilder 不是。多线程环境下性能同步需要消耗额外的性能,因此在多线程环境下,StringBuilder 会比StringBuffer更快。

因为本次基准测试是在单线程环境下,因此震惊的发现,StringBuffer和StringBuilder性能差不多,甚至StringBuffer还会略快一点点

三、源码分析

1、StringBuilder和StringBuffer的实现

分别查看StringBuilder和StringBuffer的append()方法。
1)StringBuilder#append();

   @Override
  public StringBuilder append(String str) {
      super.append(str);
      return this;
   }

2)StringBuffer#append();

  @Override
  public synchronized StringBuffer append(String str) {
      toStringCache = null;
      super.append(str);
      return this;
  }

内部都是调用父类的append方法。而它们继承同一个父类AbstractStringBuilder。
跟进父类,执行同一个拼接方法。

  public AbstractStringBuilder append(String str) {
      if (str == null)
          return appendNull();
      int len = str.length();
      ensureCapacityInternal(count + len);
      str.getChars(0, len, value, count);
      count += len;
      return this;
  }

因此,StringBuffer与StringBuilder的代码逻辑基本完全相同,唯一的差别在于StringBuffer给每一个方法都添加了synchronized同步关键字。

2、构造函数

StringBuffer 、StringBuilder 构造函数,内部都是调用父类构造器。

	public StringBuffer() {
	   super(16);
   }
    public StringBuilder() {
       super(16);
    }

父类构造器初始化,默认返回一个16个字符容量的数组。

   AbstractStringBuilder(int capacity) {
     value = new char[capacity];
  }
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值