阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

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

  • 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:336604

评论 2 条评论

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

一种融合指代消解序列标注方法在中文人名识别上的应用(上)

京东科技开发者

[行业经验] 混合云容灾混沌演练

腾讯云混沌演练平台

简单了解国密与信创的四大关系-行云管家

行云管家

信创 数据安全 国产化 国密

中国服装品牌商品计划管理系统落地难题探究

第七在线

【论文速读】| 大语言模型是边缘情况模糊测试器:通过FuzzGPT测试深度学习库

云起无垠

刘强东AI数字人开启直播带货,数字人迎来直播热潮!

青否数字人

数字人

阻碍团队使用工具的原因竟然是……

BY林子

审计 度量

盘点|《数据安全法》的62项配套「国家标准」(附下载)

极盾科技

数据安全

数据相关术语、英文翻译以及定义汇总看这里!

行云管家

数据 数据安全 企业数据

数字人抖音直播7天不封号,青否数字人8.0正式发布!

青否数字人

数字人

深蓝互动将启动《重返未来:1999》鸿蒙原生应用开发

最新动态

基于Material Design风格开源、易用、强大的WPF UI控件库

EquatorCoco

开源 UI WPF

再获权威认可!天翼云论文被IEEE/ACM CCGrid收录

天翼云开发者社区

云计算 私有云 云网关

Hive引擎底层初探

京东科技开发者

免费在线OCR识别工具TextIn Tools,开启智能学习新时代

合合技术团队

合合信息 OCR识别 文字处理 扫描全能王

DTC2024,华为云数据库创新融合大发展,打造世界级数据库!

华为云开发者联盟

数据库 华为云 华为云数据库 华为云开发者联盟 企业号2024年4月PK榜

揭秘APP自动化测试中弹窗异常处理的技术要点

测试人

App 软件测试 自动化测试 测试开发 弹窗

Apache Doris 2.1.2 版本正式发布!

SelectDB

数据库 大数据 开源 实时数仓 Doris

基于Redis实现基本抢红包算法

京东科技开发者

AI数字人直播是骗局还是机遇?揭秘数字人的真相!

青否数字人

数字人

网络审计:为什么定期检查您的网络很重要

天翼云开发者社区

云计算 网络安全 网络审计

揭秘APP自动化测试中弹窗异常处理的技术要点!

测吧(北京)科技有限公司

测试

利用1688.item_get API接口,快速定位智能手表新品,商品ID一键获取

技术冰糖葫芦

api 货币化 API 测试 pinduoduo API

面试,有时候是个运气活

老张

面试 求职

【PolarDB-X从入门到精通】 第五讲:PolarDB集中式版安装部署(源码编译部署)

阿里云数据库开源

阿里云 技术学习 polarDB 技术干货 PolarDB-X

青否AI数字人直播不封号怎么弄?

青否数字人

数字人

云主机AI服务的性能测试和优化

天翼云开发者社区

云计算 AI 云服务 云主机

Advanced RAG 03:运用 RAGAs 与 LlamaIndex 评估 RAG 应用

Baihai IDP

AI LLM 企业号 4 月 PK 榜 rag 检索增强生成

基于开源IM即时通讯框架MobileIMSDK:RainbowChat v11.5版已发布

JackJiang

网络编程 即时通讯 IM

如何爬出Kotlin协程死锁的坑?

阿里技术

Java kotlin 协程死锁

【活动报名】WorkPlus AI助理沙龙——把AI装进企业,企业级AI落地场景分享

WorkPlus

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