场景
Java
的字符串常量String
拼接字符串性能很低,到底有多低,有哪些代替方案?
说明
-
Java
的String
是常量字符串,拼接字符在JDK11
里已经做了优化。查看它生成的字节码,可以看到使用StringConcatFactory.makeConcatWithConstants
方法来拼接字符串。- 在
Intellij IDEA
里可以通过菜单View -> Show Bytecode
来查看字节码。社区版如果没有这个菜单项,需要File->Settings->Keymap-> 搜索bytecode
,之后给Show Bytecode
设置Ctrl+B
的快捷键,View
菜单里就会出来Show Bytecode
。 注意,文件要运行一次或编译过字节码
才会准确。
javafor(int i = 0; i< 10; ++i){ s1 = s1 + " world ->" + i; }
javaL11 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
编程语言的字符串连接功能。
- 在
例子
-
在以下例子里,我尽量模拟了多线程环境,这样生成的字节码才会比较真实,并且使用了循环增加时间,以便
JVM
能做深度优化。 -
给执行线程增加索引,以便能知道哪个线程在执行任务。
-
给输出字符串先添加到
StringBuffer
里,任务结束后再统一输出,避免互相穿插不同线程的输出。 -
时间单位是纳秒,可以看出执行时间按照
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