写点什么

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

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

评论 2 条评论

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

免费开源增强版 Elasticsearch 分支 OpenSearch 项目上线!现邀您加入开源社区!

亚马逊云科技 (Amazon Web Services)

【论文分享】Presto: SQL on Everything(一)

小舰

4月日更

什么是产品思维和产品意识?——课程总结

Deborah

对话亚马逊云科技 WWSO 团队四位大咖,给你讲讲他们的职场故事~

亚马逊云科技 (Amazon Web Services)

websocket简析

风翱

websocket 4月日更

算法题目解析:从一道题目看动态规划

程序员架构进阶

算法 动态规划 28天写作 4月日更

Ansible 命令行工具

耳东@Erdong

4月日更

10 亿数据量只需要 100MB 内存,Redis 的位存储为什么这么牛?

Java小咖秀

redis 原理 位运算 bit 高级特性

IPFS云算力挖矿系统开发|Filecoin算力挖矿搭建

薇電13242772558

区块链 存储

NumPy之:数据类型

程序那些事

Python 数据分析 Numpy 程序那些事

自从有了他,再也没有担心过环境部署~

MY

部署与维护

悟透前端:JavaScript ES6模块的导入导出

devpoint

ES6 export export default

你不一定全知道的四种Python装饰器实现详解

老猿Python

Python 编程语言 装饰器 程序开发

一个码砖的码农,在InfoQ写一年博客,出书了?

小傅哥

Java 设计模式 小傅哥 架构设计 码农出书

网络协议学习笔记Day1

穿过生命散发芬芳

网络协议 4月日更

王霸雄图荣华敝屣,谈笑间尽归尘土|基于Python3双队列数据结构搭建股票/外汇交易匹配撮合系统

刘悦的技术博客

数据结构 算法 Python3 匹配 股票系统

中国脑科学的十年行旅

脑极体

rrxjava原理,手持4个大厂offer的我,吊打面试官系列!

欢喜学安卓

android 程序员 面试 移动开发

借力云原生,预见医学影像“云阅片”和电子胶片的未来 | 精选案例

亚马逊云科技 (Amazon Web Services)

Android Camera开发系列:设置对焦模式模式

小驰笔记

Camera; Android;

Oracle LogMiner 数据迁移实战

yintianwen

Java 数据库 后端 数据迁移 LogMiner

Ask Me Anything #1 我是新晋CNCF TOC张磊,你有什么想问我的?

阿里巴巴云原生

容器 开发者 运维 云原生 k8s

什么情况下要招人?

石云升

团队建设 28天写作 职场经验 管理经验 4月日更

Flutter Slidable 列表侧滑菜单 列表侧滑删除

早起的年轻人

flutter

阿里开发7年大牛:只需一篇文章吃透Android多线程技术,好文推荐

欢喜学安卓

android 程序员 面试 移动开发

重读《重构2》- 搬移函数

顿晓

重构 4月日更

不要给自己留后路

小天同学

人生 个人感悟 成功 4月日更

架构训练营——作业 2

架构实战营

Python OOP-1

若尘

面向对象 oop 面向对象编程 Python编程

流程引擎介绍

上海派拉基础研发

流程

连续三年入围 Gartner 容器竞争格局,阿里云容器服务新布局首次公开

阿里巴巴中间件

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