上线前好好的代码,用户一多就卡成PPT?
前几天朋友公司出了个大事故——电商平台促销活动刚上线,订单系统突然卡死,CPU占用率飙升到100%!排查了3小时才发现,罪魁祸首竟然是一行看似无害的字符串拼接代码:
// 就是这行循环里的"+=",让系统直接崩了!String log = "";for (Order order : orderList) { log += "用户" + order.getUserId() + "下单" + order.getAmount() + "元;"; }
你是不是也这么写过?觉得"+"号简单直观,性能差不到哪去?但数据不会说谎——10万次循环拼接,用"+"要18秒,用StringBuilder只要5毫秒!(相当于博尔特和我跑100米的差距)
3组实测数据,颠覆你的认知!
我们用JDK17做了个实验:在循环里拼接字符串10万次,看看不同方式的耗时——
差距3600倍! 为什么"+"这么慢?因为String是不可变的——每次"+="都会创建新对象,循环10万次就会产生10万个临时对象,JVM垃圾回收根本忙不过来!
数据来源:CSDN博客《Java字符串拼接性能深度剖析》
什么时候用"+"?什么时候用StringBuilder?
别再一刀切啦!JDK5+早就偷偷优化了"+"——单次拼接时,编译器会自动把"+"转成StringBuilder,所以这两种写法性能几乎一样:
// 编译后完全一样!String info1 = "用户" + name + "年龄" + age; String info2 = new StringBuilder().append("用户").append(name).append("年龄").append(age).toString();
但循环里千万别用"+"! 编译器只会在单次拼接时优化,循环中每次"+="都会新建StringBuilder,相当于:
// 循环里用"+",编译器会帮你写成这样(低效!)String log = "";for (int i=0; i<100000; i++) { // 每次循环都new一个StringBuilder! log = new StringBuilder(log).append("拼接内容").toString(); }
✅ 正确做法:循环外创建一个StringBuilder,全程复用:
StringBuilder log = new StringBuilder(); // 循环外创建for (int i=0; i<100000; i++) { log.append("拼接内容"); // 直接append,不新建对象}String result = log.toString(); // 循环结束再转成String
实战案例:从"卡成PPT"到"秒级响应"
朋友公司的订单日志优化前,用"+"拼接10万条订单数据要18秒,优化后用StringBuilder只要0.005秒,还顺便解决了内存溢出问题!
❶ 低效写法(千万别学!)
String log = "订单列表:";for (Order order : orderList) { // 每次循环创建新String,内存疯狂飙升! log += "[" + order.getId() + ":" + order.getAmount() + "元],"; }
❷ 高效写法(推荐!)
// 预估长度1024字符,避免扩容(关键优化!)StringBuilder log = new StringBuilder(1024); log.append("订单列表:");for (Order order : orderList) { // 链式调用,一行搞定,全程不创建临时对象 log.append("[").append(order.getId()).append(":").append(order.getAmount()).append("元],"); }// 最后转成String,只创建1个对象String result = log.toString();
代码编辑器截图:优化前后的订单日志拼接对比
⚠️ 90%的人都踩过这3个坑!
坑1:以为"+"永远不如StringBuilder
真相:单次拼接时,"+"和StringBuilder性能几乎一样!比如name + age + "岁",编译器会自动优化成new StringBuilder().append(name).append(age).append("岁"),代码还更简洁~
坑2:忽视初始容量,导致频繁扩容
StringBuilder默认容量16,不够时会扩容为原容量×2+2(比如16→34→70...),每次扩容都要复制字符数组!正确做法:预估长度,比如拼接100个用户ID,直接new StringBuilder(1000)(每个ID约10字符)。
坑3:多线程用StringBuilder
危险! StringBuilder是非线程安全的,多线程并发append可能导致字符错乱。这时候应该用StringBuffer(方法加了synchronized锁),虽然慢一点,但安全!
记住这张图,再也不会用错!
简单总结:
单次拼接:用+(代码简洁,编译器优化)循环/频繁拼接:用StringBuilder(复用对象,指定容量)多线程拼接:用StringBuffer(线程安全,牺牲一点性能)
最后送你一个性能优化 Checklist
循环拼接字符串?先在循环外new StringBuilder!知道大概长度?指定初始容量(如new StringBuilder(1024))!多线程环境?换StringBuffer,别用StringBuilder!不确定用啥?看循环次数:10次以内用+,10次以上用StringBuilder!
快去看看你项目里的字符串拼接代码,说不定改完就能多抗10倍流量!
(图片来源:CSDN博客、博客园、Java开发社区)