写点什么

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

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

评论 2 条评论

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

gitlab registry占用存储过大问题解决

ilinux

何止一个惨字形容,水滴Java面试一轮游,壮烈了,问啥啥不会,数据库血崩,我该怎么办?

Java 编程 程序员 面试

浏览器的几种防护策略

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 安全漏洞

一个基于PoS共识算法的区块链案例

Regan Yue

区块链 共识算法 11月日更 细讲区块链

识别AI换脸!百度这项技术夺冠了!

百度大脑

人工智能 百度

新能源汽车补贴没了,行业还能快速发展吗?

石云升

学习笔记 新能源汽车 11月日更

科技热点周刊|马斯克套现 440 亿;苹果推出数字身份证;Meta 与微软合作;华为捐赠欧拉

青云技术社区

云计算 物联网

【算法技术专题】如何用Java实现一致性 hash 算法( consistent hashing )(上)

洛神灬殇

算法 一致性hash 11月日更

NodeJs 深入浅出之旅:V8 内存分配🧙‍♂️

空城机

大前端 Node 11月日更

linux下清理系统缓存并释放内存

入门小站

Linux

在线假单词随机生成器

入门小站

工具

【LeetCode】重新排序得到 2 的幂Java题解

Albert

算法 LeetCode 11月日更

华为初面+综合面试(Java技术面)附上面试题,share给大家~

Java 编程 程序员 面试

架构设计

AHUI

「架构实战营」

请问软件测试和渗透测试的区别是什么?

喀拉峻

网络安全 渗透测试

在华为云专属月,找到开启互联网第二增长曲线的一把钥匙

脑极体

你现在可以在元宇宙里 “打工”了!

CECBC

赢在2022,面试官常问的软件测试面试题总结

六十七点五

软件测试 面试题 自动化测试 经验总结 测试工程师

这一次,Google 终于对 Web 自动化下手了!

星安果

chrome 自动化

监管打压加码!虚拟货币挖矿再遭围堵 “漏网之鱼”当休

CECBC

一起听、一起看、一起唱掀起Z世代青年社交浪潮

声网

人工智能 算法 音视频

数据同步:教你如何实时把数据从 MySQL 同步到 OceanBase

OceanBase 数据库

数据库 开源 oceanbase 分布式,

你不知道的开源分布式存储系统 Alluxio 源码完整解析(下篇)

腾源会

大数据 开源

直接破防了,阿里大咖DDD(领域驱动设计)不破不立,GitHub直接霸榜,今天share给大家~

编程 程序员 领域驱动

1 分钟学会 30 种编程语言

AlwaysBeta

jodconverter实现在线预览

小鲍侃java

11月日更

你不知道的开源分布式存储系统 Alluxio 源码完整解析(上篇)

腾源会

大数据 开源 数据湖

中央银行、不平等和新技术:使用分布式账本、可编程合约和密码学的蓝图

CECBC

范学雷的专栏《深入剖析 Java 新特性》

IT蜗壳-Tango

11月日更

15 K8S之容器安全上下文

穿过生命散发芬芳

k8s 11月日更

验证码

卢卡多多

图片验证码 11月日更

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