[Java性能优化]_[时间优化]_[字符串拼接的多种方法性能分析]

场景

  1. Java的字符串常量String拼接字符串性能很低,到底有多低,有哪些代替方案?

说明

  1. JavaString是常量字符串,拼接字符在JDK11里已经做了优化。查看它生成的字节码,可以看到使用StringConcatFactory.makeConcatWithConstants方法来拼接字符串。

    • Intellij IDEA里可以通过菜单View -> Show Bytecode来查看字节码。社区版如果没有这个菜单项,需要File->Settings->Keymap-> 搜索bytecode,之后给Show Bytecode设置Ctrl+B的快捷键,View菜单里就会出来Show Bytecode。 注意,文件要运行一次或编译过字节码才会准确。
    java 复制代码
    for(int i = 0; i< 10; ++i){
        s1 = s1 + " world ->" + i;
    }
    java 复制代码
    L11
    LINENUMBER 35 L11
    ALOAD 3
    ILOAD 6
    INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;I)Ljava/lang/String; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      // arguments:
      "\u0001 world ->\u0001"
    ]
    ASTORE 3
    • 看下方法makeConcatWithConstants的注释. 但即便不是以前的new StringBuilder, 速度仍然比起老大哥StringBuilder,StringBuffer慢很多。

      Facilitates the creation of optimized String concatenation methods,hat can be used to efficiently concatenate a known number of arguments of known types,possibly after type adaptation and partial evaluation of arguments.Typically used as a bootstrap method for invokedynamic call sites, to support the string concatenation feature of the Java Programming Language.

      // 翻译大概是以下:
      有助于创建优化的字符串连接方法,这些方法可用于高效地连接已知数量和类型的参数,可能在类型适配和部分求值参数之后。通常用作invokedynamic CallSite的引导方法,以支持Java编程语言的字符串连接功能。

例子

  1. 在以下例子里,我尽量模拟了多线程环境,这样生成的字节码才会比较真实,并且使用了循环增加时间,以便JVM能做深度优化。

  2. 给执行线程增加索引,以便能知道哪个线程在执行任务。

  3. 给输出字符串先添加到StringBuffer里,任务结束后再统一输出,避免互相穿插不同线程的输出。

  4. 时间单位是纳秒,可以看出执行时间按照String > String.format > StringBuffer > StringBuilder从大到小。StringBuilder性能最高,String性能最差。StringBuffer可以用在多线程的共享变量里,StringBuilder只能用在局部变量。

TestString.java

java 复制代码
package test.example;

import org.apache.log4j.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class TestString extends TestBase{

    public static Logger logger = Logger.getLogger(TestString.class);

    @Before
    public void setUp(){
        super.setUp(logger);
        startThreadPool();
    }

    @After
    public void tearDown() {
        super.tearDown(logger);
        stopThreadPool();
    }

    private void doStringWork(int index){

        StringBuffer sInfo = new StringBuffer();
        sInfo.append(index + " =================String +===================").append("\n");

        String s1 = "hello";
        long start = System.nanoTime();
        for(int i = 0; i< 10; ++i){
            s1 = s1 + " world ->" + i;
        }
        long end = System.nanoTime();
        sInfo.append(index + " String +: "+s1).append("\n");
        sInfo.append(index + " duration :"+(end-start)).append("\n");

        // -- 可变字符串;用在局部变量上; 不能用在共享变量上。性能比 StringBuffer 高
        // 直接调用父类 AbstractStringBuilder.append
        //   -- 方法没有 synchronized 关键字
        sInfo.append(index + "=================StringBuilder===================").append("\n");

        StringBuilder sb = new StringBuilder();
        sb.append("hello");
        start = System.nanoTime();
        for(int i = 0; i< 10; ++i){
            sb.append(" world ->").append(i);
        }
        end = System.nanoTime();
        sInfo.append(index + " StringBuilder: "+sb.toString()).append("\n");
        sInfo.append(index + " duration :"+(end-start)).append("\n");

        // -- 可以用在共享变量上;方法有 synchronized 关键字。
        // append方法加 synchronized 之后 调用父类 AbstractStringBuilder.append
        sInfo.append(index + " =================StringBuffer===================").append("\n");
        start = System.nanoTime();
        StringBuffer bu = new StringBuffer();
        bu.append("hello");
        for(int i = 0; i< 10; ++i){
            bu.append(" world ->").append(i);
        }
        end = System.nanoTime();
        sInfo.append(index + " StringBuffer: "+bu.toString()).append("\n");
        sInfo.append(index + " duration: "+(end-start)).append("\n");

        // -- format创建的是不可变字符串; 需要分析字符串格式,代码优雅,但是性能不佳。
        //   -- 调用 new Formatter().format(format, args).toString(); 来格式化.
        sInfo.append(index + " ================= String.format===================").append("\n");
        start = System.nanoTime();
        String s2 = "hello";
        for(int i = 0; i< 10; ++i){
           s2 = String.format("%s%s%d", s2, " world ->",i);
        }
        end = System.nanoTime();
        sInfo.append(index + " String.format: "+s2).append("\n");
        sInfo.append(index + " duration: "+(end-start)).append("\n");

        synchronized (this){
            logger.info(sInfo.toString());
        }

    }

    @Test
    public void testStringConcat(){

        var number = getMaximumPoolSize();
        var endLatch = startCountDown(number);
        for(int i = 0; i< number; ++i){
            final var index = i;
            executor.execute(()->{
                doStringWork(index);
                endLatch.countDown();
            });
        }

        waitCountDown(endLatch);
    }

}

TestBase

java 复制代码
package test.example;

import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.spi.LoggerRepository;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;




public class TestBase {

    // 自定义线程池参数
    protected int corePoolSize = 5;           // 核心线程数
    protected int maximumPoolSize = 10;       // 最大线程数
    protected long keepAliveTime = 60;        // 空闲线程存活时间
    protected TimeUnit unit = TimeUnit.SECONDS; // 时间单位
    protected BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); // 任务队列

    protected ThreadPoolExecutor executor;
    protected ScheduledExecutorService scheduler;

    public long startRecord(){
        return System.nanoTime();
    }

    public long endRecord(){
        return System.nanoTime();
    }

    public void pDuration(Logger logger, long start, long end, String key){
        logger.info(key+" duration: "+(end-start));
    }

    public void setUp(){
        setUp(null);
    }

    public void tearDown(){
        tearDown(null);
    }

    protected void startThreadPool(){
        // 创建线程池
        executor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                new NamedThreadFactory("MyCustomPool") // 使用自定义线程工厂,指定线程名称
        );
    }

    protected void stopThreadPool(){
        if(executor != null){
            executor.shutdown();
            executor = null;
        }
    }

    protected void startScheduleService(){
        scheduler = new ScheduledThreadPoolExecutor(
                corePoolSize,
                new NamedThreadFactory("ScheduledPool")
        );
    }

    protected void stopScheduleService(){
        if(scheduler != null){
            scheduler.shutdown();
            scheduler = null;
        }
    }

    protected void waitScheduleTask(){
        try {
            if (!scheduler.awaitTermination(20, TimeUnit.SECONDS)) {
                scheduler.shutdownNow();
            }
        } catch (InterruptedException e) {
            scheduler.shutdownNow();
        }
    }

    protected void setUp(Logger logger){

        assert(logger != null);

        LoggerRepository repository = logger.getLoggerRepository();
        ConsoleAppender appender = new ConsoleAppender(new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN));
        repository.getRootLogger().addAppender(appender);

    }

    protected int getMaximumPoolSize(){
        return maximumPoolSize;
    }

    protected int getCorePoolSize(){
        return maximumPoolSize;
    }

    protected CountDownLatch startCountDown(int size){
        return new CountDownLatch(size);
    }

    protected void tearDown(Logger logger){
        logger.removeAllAppenders();
    }

    protected void waitCountDown(CountDownLatch endLatch){
        try {
            endLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 自定义线程工厂(带名称前缀)
    public static class NamedThreadFactory implements ThreadFactory {
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        public NamedThreadFactory(String poolName) {
            this.namePrefix = poolName + "-thread-";
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement());
            if (t.isDaemon()) {
                t.setDaemon(false); // 设置为非守护线程
            }
            if (t.getPriority() != Thread.NORM_PRIORITY) {
                t.setPriority(Thread.NORM_PRIORITY); // 设置默认优先级
            }
            return t;
        }
    }
}

输出

复制代码
0 [MyCustomPool-thread-2] INFO test.example.TestString  - 1 =================String +===================
1 String +: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
1 duration :9604800
1=================StringBuilder===================
1 StringBuilder: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
1 duration :2000
1 =================StringBuffer===================
1 StringBuffer: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
1 duration: 4000
1 ================= String.format===================
1 String.format: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
1 duration: 480400

2 [MyCustomPool-thread-3] INFO test.example.TestString  - 2 =================String +===================
2 String +: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
2 duration :9589000
2=================StringBuilder===================
2 StringBuilder: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
2 duration :4100
2 =================StringBuffer===================
2 StringBuffer: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
2 duration: 6000
2 ================= String.format===================
2 String.format: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
2 duration: 1303700

3 [MyCustomPool-thread-4] INFO test.example.TestString  - 3 =================String +===================
3 String +: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
3 duration :9906200
3=================StringBuilder===================
3 StringBuilder: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
3 duration :1800
3 =================StringBuffer===================
3 StringBuffer: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
3 duration: 3900
3 ================= String.format===================
3 String.format: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
3 duration: 555300

4 [MyCustomPool-thread-5] INFO test.example.TestString  - 4 =================String +===================
4 String +: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
4 duration :9591800
4=================StringBuilder===================
4 StringBuilder: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
4 duration :2300
4 =================StringBuffer===================
4 StringBuffer: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
4 duration: 3200
4 ================= String.format===================
4 String.format: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
4 duration: 701200

4 [MyCustomPool-thread-1] INFO test.example.TestString  - 0 =================String +===================
0 String +: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
0 duration :9591400
0=================StringBuilder===================
0 StringBuilder: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
0 duration :1900
0 =================StringBuffer===================
0 StringBuffer: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
0 duration: 3200
0 ================= String.format===================
0 String.format: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
0 duration: 690500

5 [MyCustomPool-thread-3] INFO test.example.TestString  - 6 =================String +===================
6 String +: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
6 duration :17800
6=================StringBuilder===================
6 StringBuilder: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
6 duration :1000
6 =================StringBuffer===================
6 StringBuffer: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
6 duration: 156700
6 ================= String.format===================
6 String.format: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
6 duration: 1153400

5 [MyCustomPool-thread-2] INFO test.example.TestString  - 5 =================String +===================
5 String +: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
5 duration :694000
5=================StringBuilder===================
5 StringBuilder: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
5 duration :2000
5 =================StringBuffer===================
5 StringBuffer: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
5 duration: 1900
5 ================= String.format===================
5 String.format: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
5 duration: 1925000

5 [MyCustomPool-thread-4] INFO test.example.TestString  - 7 =================String +===================
7 String +: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
7 duration :383000
7=================StringBuilder===================
7 StringBuilder: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
7 duration :1200
7 =================StringBuffer===================
7 StringBuffer: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
7 duration: 3100
7 ================= String.format===================
7 String.format: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
7 duration: 810100

6 [MyCustomPool-thread-1] INFO test.example.TestString  - 9 =================String +===================
9 String +: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
9 duration :31200
9=================StringBuilder===================
9 StringBuilder: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
9 duration :1800
9 =================StringBuffer===================
9 StringBuffer: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
9 duration: 2800
9 ================= String.format===================
9 String.format: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
9 duration: 1051100

6 [MyCustomPool-thread-5] INFO test.example.TestString  - 8 =================String +===================
8 String +: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
8 duration :34600
8=================StringBuilder===================
8 StringBuilder: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
8 duration :1700
8 =================StringBuffer===================
8 StringBuffer: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
8 duration: 2100
8 ================= String.format===================
8 String.format: hello world ->0 world ->1 world ->2 world ->3 world ->4 world ->5 world ->6 world ->7 world ->8 world ->9
8 duration: 1876100


Process finished with exit code 0

参考

  1. 配置IDEA和Android Studio的JDK

  2. JDK8-Java8-JavaSE8新特性和增强功能

  3. JDK7-Java7-JavaSE7新特性和增强功能

相关推荐
亲爱的马哥3 分钟前
重磅更新 | 填鸭表单TDuckX2.9发布!
java
Java中文社群4 分钟前
26届双非上岸记!快手之战~
java·后端·面试
whitepure8 分钟前
万字详解Java中的面向对象(二)——设计模式
java·设计模式
whitepure11 分钟前
万字详解Java中的面向对象(一)——设计原则
java·后端
摘星编程36 分钟前
私有化部署全攻略:开源模型本地化改造的性能与安全评测
性能优化·私有化部署·开源模型·安全防护·企业级ai
2301_7930868738 分钟前
SpringCloud 02 服务治理 Nacos
java·spring boot·spring cloud
回家路上绕了弯1 小时前
MySQL 详细使用指南:从入门到精通
java·mysql
小七rrrrr1 小时前
动态规划法 - 53. 最大子数组和
java·算法·动态规划
自由的疯1 小时前
在 Java IDEA 中使用 DeepSeek 详解
java·后端·架构