2天时间,聊今年最热的 Agent、上下文工程、AI 产品创新等话题。2025 年最后一场~ 了解详情
写点什么

三个常见的代码性能优化方式

  • 2019-04-28
  • 本文字数:3874 字

    阅读完需:约 13 分钟

三个常见的代码性能优化方式

编写有效率的代码是我们的一项基本技能。我们千万不要忽视代码的性能要求。越早考虑性能问题,需要支付的成本就越小,带来的价值就越大,不要等到出现性能问题时,才去临时抱佛脚。如果前期没有看重代码的性能问题,那么后期我们就要付出加倍的精力去维护和重构代码。


代码的性能并不是可以多块地进行加减乘除,而是如何管理内存、磁盘、网络、内核等计算机资源,已达到性能最优化。


在这篇文章里,我选了三个常见且实用的代码性能优化方式,供你参考和借鉴。

让接口保持简单直观的两个小技巧

设计接口之所以难,在于接口对稳定性的要求比较高。要想保证接口的稳定性,最有效的方法就是让接口设计得简单直观一些。在工作中,我总结了两个小技巧供你参考使用。

学会拆解问题

设计软件接口,要从实际的问题出发,只有这样,我们才能找到一条清晰的主线。围绕这条主线展开设计,就可以有效地避免需求膨胀和过渡设计。


拆解问题,需要遵循两个原则——相互独立,完全穷尽。


比如说,是否可以授权一个用户使用某一个在线服务呢?这个问题就可以分解为两个小问题:


1、该用户是否为已注册的用户?


2、该用户是否持有正确的密码?


我们可以使用思维导图来描述这个分解。



这种划分其实是有问题的。因为只有已经注册的用户,才会持有正确的密码。而且,只有持有正确密码的用户,才能够被看作是注册用户。这两个小问题之间,存在着依赖关系,就不能算是“相互独立”。


我们要消除掉这种依赖关系,这样需要两个层次的表达。第一个层次问题是,该用户是否为已注册的用户?这个问题,可以进一步分解为两个更小的问题:用户持有的用户名是否已注册? 用户持有的密码是否匹配?


1、该用户是否是已注册的用户?


用户名是否已注册?


用户密码是否正确?



但这样还是有缺陷的。如果一个服务,对所有的注册用户开放,上面的分解就是完备的。否则,我们就漏掉了一个重要的内容,不同的注册用户,可以访问的服务可能是不同的。也就是说如果没有访问的权限,那么即使用户名和密码正确也无法访问相关的服务。


如果我们把漏掉的加上,这个问题的分解可以进一步表示为:


1、该用户是否是已注册的用户?


用户名是否已注册?


用户密码是否正确?


2、该用户是否有访问的权限?



到这一步,我们就会有一个清晰的思路了。

一个接口解决一件事情

这里所说的“事情”,其实是在某一个层级上的一个职责,比如授权用户访问是一件完整、独立的事情。有了逻辑级别,我们才能分解问题,接口之间才能建立联系。


对于一件事的划分,我们要注意三点。


1、一件事就是一件事,不是两件事,也不是三件事。


2、这件事是独立的。


3、这件事是完整的。


我们以一段代码为例,看一下如果接口不明确会产生怎样的后果。


/** * A {@code HelloWords} object is responsible for determining how to say * "Hello" in different language. */class HelloWords {    private String language = "English";    private String greeting = "Hello";
// snipped
/** * Set the language of the greeting. * * @param language the language of the greeting. */ void setLanguage(String language) { // snipped }
/** * Set the greetings of the greeting. * * @param language the greetings of the greeting. */ void setGreeting(String greeting) { // snipped }
// snipped }
复制代码


这段代码涉及两个要素,一个是语言(英语、汉语等),一个是问候语(Hello、你好等),它抽象出了这两个要素。使用 setLanguage()设置问候的语言,使用 setGreeting()设置问候的问候语。但这样的设计对用户是不友好的。因为 setLanguage()和 setGreeting()这两个方法,都不能表达一个完整的事情。只有两个方法合起来,才能表达一件完整的事情。


这种互相依赖的关系,会导致很多问题。 比如说:


1、使用时,应该先调用哪一个方法?


2、如果语言和问候语不匹配,会出现什么情况?


3、实现时,需不需要匹配语言和问候语?


4、实现时,该怎么匹配语言和问候语?


所以我们应当牢记,接口应该尽可能只解决一件事情,如果实在做不到,就需要减少依赖关系。


想了解更多有关接口设计的内容,请点击:怎么设计一个简单又直观的接口?

学会使用 JMH,避免性能陷阱

我们如何才能知道自己编写的代码的性能呢?事实上,Java 提供了一个性能测试工具 JMH,它可以直观地帮助我们查看代码的性能缺陷和陷阱。

JMH 的使用方法

首先,使用 Maven 工具建立一个基准测试项目:


mvn archetype:generate \          -DinteractiveMode=false \          -DarchetypeGroupId=org.openjdk.jmh \          -DarchetypeArtifactId=jmh-java-benchmark-archetype \          -DgroupId=com.example \          -DartifactId=myJmh \          -Dversion=1.0
复制代码


然后编译基准测试:


cd myJmh$ mvn clean install
复制代码


最后运行编译测试:


cd myJmh$ Java -jar target/benchmarks.jar
复制代码


下面是运行结果,我们需要注意到 Score 这一栏,它显示的是每秒可以执行的基准测试方法的次数。次数越多,效率越高。


Benchmark                Mode  Cnt        Score          Error  UnitsMyBenchmark.testMethod  thrpt   25        35.945 ▒       0.694  ops/s
复制代码


下面我们通过运行三个字符串 String、StringBuilder 和 StringBuffer,来看下这三个字符串的性能差异。为了方便对比,JMH 的测试结果,都写在了注释里。


// JMH throughput benchmark: about 32 operations per second@Benchmarkpublic String measureStringApend() {    String targetString = "";    for (int i = 0; i < 10000; i++) {        targetString += "hello";    }
return targetString;}
// JMH throughput benchmark: about 5,600 operations per second@Benchmarkpublic String measureStringBufferApend() { StringBuffer buffer = new StringBuffer(); for (int i = 0; i < 10000; i++) { buffer.append("hello"); }
return buffer.toString();}
// JMH throughput benchmark: about 21,000 operations per second@Benchmarkpublic String measureStringBuilderApend() { StringBuilder builder = new StringBuilder(); for (int i = 0; i < 10000; i++) { builder.append("hello"); }
return builder.toString();}
复制代码


你可能会看到,使用 String 的性能是最差的,StringBuilder 的字符串连接操作,比使用 String 的操作快了近 200 倍,而 StringBuffer 的字符串连接操作,更是快了近 700 倍。


为什么 String 的效率如此慢?这是因为每一个字符串连接的操作,都需要创建一个新的 String 对象,然后再销毁,再创建。这种模式对 CPU 和内存消耗都比较大。


StringBuilder 为什么比 StringBuffer 还要快呢?StringBuffer 的字符串操作是多线程安全的,而 StringBuilder 的操作就不是。如果我们看这两个方法的实现代码,除了线程安全的同步以外,几乎没有差别。


通过上面的基准测试,我们可以得出这样的结论:


1、频繁的对象创建、销毁,有损代码的效率;


2、减少内存分配、拷贝、释放的频率,可以提高代码的效率;


3、即使是单线程环境,使用线程同步依然有损代码的效率。


但这并不意味着使用 StringBuilder 会更好,想要查看更多基准测试和结论,请点击:有哪些招惹麻烦的性能陷阱?

超越线程同步的技巧

我们都知道,线程同步有损效率。在实际工作中,我们只要打破下面的任何一个条件,就不需要使用线程同步了:

使用单线程;

1、不关心共享资源的变化;


2、没有改变共享资源的行为。


3、应用到具体的工作场景中,又该怎么避免线程同步呢?

学会使用 final 关键字

Java 里面的 final 关键字,可以把变量改为不可变的量。在软件环境里,不可变,就意味着一旦实例化,就不再改变。


比如下面这段代码是没有使用 final 关键字的。如果只有一个线程,这段代码就没有问题。但是,如果有两个线程,一个线程读,一个线程写,就会出现竞争状况,返回不匹配的语言环境和问候语。


class HelloWords {         private String language = "English";         private String greeting = "Hello";
void setLanguage(String language) { this.language = language; }
void setGreeting(String greeting) { this.greeting = greeting; }
String getLanguage() { return language; }
String getGreeting() { return greeting ; }}
复制代码


如果我们使用了 final 关键字,类变量只能被赋值一次,而且只能在实例化之前被赋值。这样的变量,就是不可变的量。如果一个类的所有的变量,都是不可变的,那么这个类也是不可变的。


class HelloWords {      private final String language;      private final String greeting;
HelloWords(String language, String greeting) { this.language = language; this.greeting = greeting; }
String getLanguage() { return language; }
String getGreeting() { return greeting ; }}
复制代码


所以,我们要养成一个习惯,看到声明的变量,就要琢磨,这个变量能不能声明成不可变的量?有没有办法修改接口设计或者实现代码,把它改成不可变的量?设计一个类时,要优先考虑,这个类是不是可以设计成不可变的类?这样就可以避免很多不必要的线程同步,让代码的效率更高,接口更容易使用。


更多有关超越线程同步的内容,请点击:高效率,从超越线程同步开始!进行留言互动。


你在工作中还会遇到哪些有关代码的性能问题?又是如何解决的呢?欢迎你在评论区与我分享你的经验和心得。


文章选自《代码精进之路》


2019-04-28 11:337101

评论 2 条评论

发布
用户头像
“使用 String 的性能是最差的,StringBuilder 的字符串连接操作,比使用 String 的操作快了近 200 倍,而 StringBuffer 的字符串连接操作,更是快了近 700 倍”---这里是不是写的有问题?stringbuilder明明是最快的,为什么才200倍?而stringbuffer反而700倍。。。
2019-05-20 09:26
回复
用户头像
好像不错 收藏先 抽空看
2019-04-28 14:40
回复
没有更多了
发现更多内容

全面的采样器Native Instruments Maschine 音乐灵感源源不断

Rose

Bricsys BricsCAD 25 for mac 强大CAD绘图软件

晨光熹微

xHistory for mac 终端记录管理工具

晨光熹微

Eudic欧路词典 for Mac 英语词典翻译查询工具

晨光熹微

使用 ftrace 跟踪内核丢包问题定位的实践

天翼云开发者社区

网络安全

Kaleidoscope for Mac(图片和文本差异比较工具)

Geek贝

专升本服务管理平台(源码+文档+讲解+演示)

深圳亥时科技

VoiceInk for mac AI实时语音转文本

晨光熹微

Serial Box for Mac 软件序列号查询软件

晨光熹微

Insert into select 语句中 prepare 时间解析

TiDB 社区干货传送门

故障排查/诊断

专业级网络调试与抓包分析工具Proxyman Premium for Mac

Geek贝

黑苹果必备驱动工具 Hackintool for Mac中文免费

Rose

VoiceInk for mac (AI实时语音转文本)1.33(133) 免激活版

Rose

一键部署微软Office Office Tool Plus中文便携免费版

Rose

# 我的 TiDB 之旅:从初识到深入

TiDB 社区干货传送门

TiDB第四届征文-运维开发之旅

Typinator for Mac v9.2 文本短语快速输入工具 激活版

Rose

XnViewMP for Mac 图片浏览查看器

晨光熹微

XnViewMP 下载 XnViewMP mac

能源转型委员会(ETC)发布新简报:应对能源转型中的全球贸易挑战

财见

Univar Solutions发布2024年ESG报告

财见

托管云

等保测评

Print to PDF Pro for Mac v5.8.5 PDF文件打印软件 激活版

Rose

国产化迁移之TiDB 两地三中心高可用实践

TiDB 社区干货传送门

迁移 管理与运维 应用适配 7.x 实践 TiDB第四届征文-业务场景实战

YashanDB定期维护与升级指南,保持系统稳定

数据库砖家

首度公开|网易伏羲:去中心化,从单智能体到群体智能的跃迁

网易伏羲

网易伏羲 数字游民 具身智能 群体智能 AOP编程

tidb+springAI:一个基于国产化分布式数据库的RAG项目实战

TiDB 社区干货传送门

8.x 实践 TiDB Vector TiDB第四届征文-运维开发之旅

TechSmith Snagit for Mac v2025.2.1 AI驱动的截屏软件 激活版

Rose

AI 助手增强JetBrains PhpStorm 2025.1.2破解版及密钥下载 Mac/win

Rose

AI英语听力APP的开发

北京木奇移动技术有限公司

软件外包公司 AI听力 AI英语学习

一文讲透,如何快速完成埋点与用户行为分析产品选型

ClkLog

开源 sdk 用户行为分析 客户画像 埋点分析

翻遍全网不如问问小艺:轻松get父亲节送礼推荐

最新动态

YashanDB FAQ:常见问题与答疑

数据库砖家

三个常见的代码性能优化方式_编程语言_范学雷_InfoQ精选文章