程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

系统性能优化与Java代码编写性能考虑

balukai 2025-03-18 19:47:14 文章精选 6 ℃

一 、性能与性能优化

性能指的是衡量系统是否能满足用户及技术管理需求的一组指标。

性能优化是为了使这些指标能够达到用户及管理的目标,而对系统进行

的一系列改进过程。

作为信息系统的一项重要工作,性能优化过程将贯穿系统开发和应用的

整个生命周期。如何用更少的资源提供更好的系统服务,成本利益最大化,

并避免IT风险中的系统容量不足等,是亟待我们在软件开发过程中需要重视

并考虑解决的问题,提高系统的处理效率,提高系统的性能,保障生产系统

的运行稳定。

性能优化总则

o 性能调优一个长期的过程,设计开发阶段要提起充分的重视。性能调优 的意义在于充分利用资源,提高系统响应时间和并行度,确保业务正常 高效运行,提高用户/客户满意度。


o 在进行大规模的企业分布式应用开发时,性能问题必须得到高度重视, 很多细节问题的处理不当都会造成最终应用性能的降低,严重影响用户 的使用体验。因此,必须在从设计、开发、测试到应用的打包部署的整 个过程中始终将系统的性能优化作为要点问题加以考虑,避免那种在系 统最终部署后发现性能问题后被动采取调优措施的情况。

IT系统性能重要度量指标




吞吐量

o TPS/TPM指应用在1秒/1分钟内可处理的交易数量,用于评价系统的整体容量情况。 在评价系统容量时, 该指标一般还会参考系统资源占用情况。

o 如AP=60%±5%或DB=80%±10%情况下的TPS/TPM值。

o 有些互联网公司区分TPS和QPS。这里TPS指的是应用后端每秒平均可以完成的事务个数,反映 了应用在数据库处理、 CPU资源使用等方面的能力;而QPS指的是每秒平均可以完成的查询个数。

o 并发量。并发量不可简单地归入TPS/TPM等交易处理的评价指标体系中。对于部分场景(例如秒杀、 抢券)而言,并发量也是评价系统容量的重要指标。通过测得的并发量合理设置流控策略,可以有效 保障系统在高压力/突发压力下平稳运行。

资源利用率

o CPU

o 期望值:随不同系统有差异,一般维持在60%左右较好; 峰值: 超过90%认为系统满负荷运行。 目前系统超过70%,系统监控将会有短信告警信息。

o 内存

o 期望值:不同系统的内存管理机制不一样,没有一个统一的指标;峰值:以满足业务功能基本 要求为准

o 网络带宽

o 期望值:随不同系统有差异,应用对网络的要求相对明确,包括大小、时间;峰值:以满足业 务功能、总体成本要求为准。

o 响应时间

o 平均响应时间指应用在一段时间内的平均响应时间,用于评价系统的整体响应情况。平均响应时间值 的大小显示了应用在性能方面的优劣,小的响应时间代表应用有好的处理能力。

o 如新一代联机交易简单交易耗时指标100毫秒,复杂交易300毫秒。

o 响应时间平滑度,指应用在一段时间内响应时间的平稳程度,如果实际的响应时间曲线过多出现据齿 形状,虽然应用的平均响应时间满足性能指标要求,也表明应用可能存在潜在的不稳定性。

性能优化原则

规划较好的方法学、采用有效的改进措施和手段是实现成功性能优化的关键。根据系统的效用、 目的,应该采用不同的调优策略,性能优化工作遵循如下原则:

■目标原则

在开始应用优化之前,首先应该定义明确的优化目标。当满足优化目标时就可以停止优化工作, 避免再消耗其他代价来继续进行应用优化工作。

■架构设计优先

在系统建设初期,设计支持性能优化企业架构和系统架构将最大程度支持信息系统各个生命周期 的性能优化工作。如果软件、系统已成为产品,此时再进行系统调整,则耗费的精力最多,而收 益最小,而且很多架构和设计上的不合理几乎是无法修正的。所以要求系统在最初的架构设计阶 段就充分考虑将来运行的性能问题。

■概念原型验证

概念原型验证是针对具体需求量身定制,而进行产品、方案的关键功能方面的验证在有条件的前 提下,要求项目组尽量完成。

■自底向上

当应用系统未能达到预期的性能指标时,或系统的性能能够达到预期的性能指标,但性能随 着时间的推进而降低,都需要对应用系统进行优化。对应用系统进行优化时,需要从应用的 底层入手,一直向上搜索,并解决发现的瓶颈问题。

■软件优先

应用的性能未能满足要求时,不应该首先考虑增加硬件设备;应该首先考虑从现有的应用和 应用服务器榨取最大的性能,在此之后才考虑添加硬件设备。从长远来看,单纯靠添加硬件 来提高性能很难获得好效果;虽然有可能暂时解决眼前的性能危机,但问题仍旧存在,一旦 负载增加了又会出现。

■二八原则

在对应用进行性能优化的时候,应该遵循“二八原则”。所谓“二八原则”是指在应用中20%的问 题导致了应用80%以上的性能问题;这20%的问题被称为影响应用性能的瓶颈。我们在对应用 进行优化的时候,应该着力解决影响性能的瓶颈。



二、代码编写

复杂度

算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法, 也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。

那么我们应该如何去衡量不同算法之间的优劣呢?主要还是从算法所占用的「时间」和「空间」两个 维度去考量。

o 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。

o 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。


见的时间复杂度/空间复杂度量级(依次增长)

o 常数阶O(1)

o 对数阶O(logN)

o 线性阶O(n)

o 线性对数阶O(nlogN)

o 平方阶O(n^2)

o 立方阶O(n^3)

o K次方阶O(n^k)

o 指数阶(2^n)

Java数据结构——简介




Java数据结构——ArrayList和HashMap的使用


在行内代码中,用得最多的数据结构接口非ArrayList和HashMap莫属。两者侧重点各有不同:

o ArrayList 是有序的,其顺序插入的时间复杂度O(1),随机插入的时间复杂度O(n),查询的 时间复杂度是O(n),并且元素是可以不相同的

o HashMap 是无序的,其插入的时间复杂度O(1),查询的时间复杂度O(1),并且key是不可 以重复的(重复key会覆盖原有value)

int max = 100000;
int searchTimes = 10000;
List list = new ArrayList<>();
Map map = new HashMap<>();
for (int i = 0; i < max; i ++) {
list.add(i);
map.put(i, i);
}
List searchList = new ArrayList<>();
for (int i = 0; i < searchTimes; i++) {
searchList.add((int)(Math.random() * max));
}
long t1 = System .currentTimeMillis();
for (int item : searchList) {
list.indexOf(item);
}
long t2 = System .currentTimeMillis();
for (int item : searchList) {
map.get(item);
}
long t3 = System .currentTimeMillis();
System .out.println(String.format("finding in arraylist costs %s ms", t2-t1)); 
System .out.println(String.format("findint in hashmap cost %s ms", t3-t2));


相比之下, ArrayList比较适合于关注元素出入顺序的

场景,不适用于查询场景,可以用于操作记录、数据

顺序读取解析等场景;

HashMap比较适用于不关注于元素进入顺序,并且随

机查询的场景,可用于参数查询、索引/映射查询等场

景。


上面代码以在10w个元素中随机查询1w次为例,执行结果:

finding in arraylist costs 2447 ms

findint in hashmap cost 3 ms


Java数据结构——ArrayList和HashMap的初始化与扩容


ArrayList和HashMap底层都是基于Java的数组Array实现。对于Java而言, Array的大小是固定 的, ArrayList和HashMap却不是,那Java是怎么实现的呢?

o ArrayList的默认初始化大小是10,可以自由指定size大小。

o HashMap的默认初始化大小是16,大小必须是2的次方,可以指定初始capacity和 loadFactor (默认0.75)。


o ArrayList执行add操作时,如果判断空间不足,会执行grow方法扩展到原大小的1.5倍, 并将元素/地址挨个拷贝到新对象中。

o HashMap执行put操作时,如果put结束后判断size > capacity* loadFactor会执行扩容,每 次扩容为之前大小的2倍,并将元素/地址挨个拷贝到新对象中。


因此,如果这两个对象出现可预见地频繁扩容,建议初始化时指定好目标大小。


Java数据结构——ArrayList和LinkedList

顾名思义, ArrayList是基于Array数组的,而LinkedList是基于链表的。


两者相同点

o ArrayList 和LinkedList都实现了List接口,都是有序的,其顺序插入的时间复杂度O(1),查 询的时间复杂度是O(n),并且元素是可以不相同的


两者差异

o ArrayList随机插入的时间复杂度O(n),随机删除元素的时间复杂度O(n) ,扩容需要做数组 拷贝动作,无法自动缩容


o LinkedList随机插入的时间复杂度是O(1) (如果计算index的时间则也是O(n)),随机删除 元素的时间复杂度是O(1) (如果计算index的时间则也是O(n)),并且可以任意扩缩容

Java数据结构—— Hashtable和ConcurrentHashMap

HashMap的高效查询的特点,使得它经常用于参数集合的内存存放或本地索引构建等场景。然 而,其实现却是非线程安全的!

o HashTable是一个线程安全的类,它使用synchronized来锁住整个对象来实现线程安全,即每 次锁住整张表让线程独占,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。


o ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的 时候能够将锁的粒度保持地尽量地小,允许多个修改操作并发进行,其关键在于使用了锁分 段技术。它使用了多个锁来控制对hash表的不同部分进行的修改。


o HashMap虽然是非线性安全的,但如果有些并发场景下我们能容忍不同线程同时操作同一个 key行不行呢?

o 答案是否定的。 HashMap的非线程安全特性不仅体现在并发执行时的结果不可预知,还 可能会导致死循环问题!行内已有类似生产事件发生。


因此,如果要想在高并发场景下使用Map做key查询,最安全而又有效的方法是使用


java.util.ConcurrentHashMap

synchronized与Loc

o synchronized关键字

是Java提供的内置锁机制, Java中的每个对象都可以用作一个实现同步的锁(内置锁或者监视器 Monitor),线程在进入同步代码块之前需要或者这把锁,在退出同步代码块会释放锁。而 synchronized这种内置锁实际上是互斥的,即每把锁最多只能由一个线程持有。

针对非静态方法加锁时,其对当前对象加锁;针对静态方法加锁时,其对当前类(也是对象, 万物皆对象)加锁。


o Lock接口,其JDK实现类是ReentrantLock

Lock接口提供了与synchronized相似的同步功能,和synchronized (隐式的获取和释放锁,主要 体现在线程进入同步代码块之前需要获取锁退出同步代码块需要释放锁)不同的是, Lock在使 用的时候是显示的获取和释放锁。虽然Lock接口缺少了synchronized隐式获取释放锁的便捷性, 但是对于锁的操作具有更强的可操作性、可控制性以及提供可中断操作和超时获取锁等机制。

o ReadWriteLock接口,其JDK实现类是ReentrantReadWriteLock

ReadWriteLock提供了两个方法,一个用来获取读锁,一个用来获取写锁。也就是说将文件的读 写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。读与读之间并 不互斥,读与写、写与写之间是互斥的,这样在读多写少的情况下,可以进一步提升调度效率。 ConcurrentHashMap的实现就是基于ReadWriteLock。

Java数据结构—— HashMap和HashSe

HashSet的本质就是HashMap!其底层实现将Set元素看成是HashMap的key,而value是默认赋予 的Object对象,使用者并不需要关注。


由于HashMap的key是唯一的,因此HashSet的元素也是唯一的。 HashSet可以非常便捷地用于元 素去重,无论是从代码简洁程度还是执行效率都无可挑剔,如计算文章中出现的不同词汇清单。 当然,如果除了去重的功能,还想统计一下重复次数,那还是得请回HashMap了,如统计词语在文章中的词频


Java数据结构——优先队列PriorityQueue

熟悉堆排序的同学们,应该会对优先队列PriorityQueue的用法感到很欣喜。 PriorityQueue为我们提 供了一个大顶堆的默认实现逻辑,并且提供了传入Comparator接口实现做扩展,只要可以比较的元 素(包括自定义类的对象)都可以使用这个JDK默认实现来进行堆排序了

适用场景:随时获得当前最大/最小数据,或者获得第K大/第K小/前K大/前K小的数据。

o add和offer,添加元素,前者在插入失败时抛出异常,后则则会返回false。添加元素的复杂度是 O(log(n)) ,需要对堆进行调整。

o element和peek,获取但不删除队首元素,当方法失败时前者抛出异常,后者返回null。这个复杂 度是O(1)

o remove和poll,获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回null。删除元 素的复杂度是O(log(n)),需要对堆进行调整。


PriorityQueue是非线程安全的,所以Java提供了PriorityBlockingQueue (实现BlockingQueue接口)用 于Java多线程环境。

Java数据结构——TreeMap

TreeMap是JDK中针对红黑树提供了一种实现,它实现了Map接口,因此其中的key是唯一的;其在 维护的时候同时针对key大小做了特殊处理,使用者可以根据key的顺序找到临近大/小的其他key,或 者获得key大于/小于某个指定值的子集。



适用场景:随时获得可变有序集合中任意一个元素的前K个元素/后K个元素/所有大于该元素/所有小 于该元素的子集。

o Put,增加新元素,时间复杂度为O(log(n))。

o get,获取元素,无论成功与否,时间复杂度为O(log(n))

o containsKey,判断key是否存在,无论成功与否,时间复杂度为O(log(n))

o
higherEntry/lowerEntry/ceilingEntry/floorEntry操作,时间复杂度为O(log(n))

o headMap/tailMap直接获得子集,可用于遍历


字符串拼接与日志打印


Substring的使用

在Java语言中,字符串是常量不能被修改。 JDK为字符串提供了获取子串的API——substring。在1.6 版本及以下的JDK中,这个方法不会在内存中开辟一块区域记录新的子串,而是很“节俭”地持有了原 字符串中char数组的引用。

这样做固然有一定好处,但不明真相的群众不知道它的原理时,可能写出极其消耗内存的代码,甚 至可能导致内存不够用。例如,在1G的字符串中截取1个Byte的子串并持有其引用,原有1G的字符串 会因此无法被回收。所以为安全起见,在JDK6中, substring一般要外面套一层new String(String)的构 造方法,以确保新的子串断开与原字符串的引用。

在JDK8以后,这个问题就不存在了,其原因是jdk8的substring方法已经默认加入了new String(String) 的操作。

空间换时间

动态规划(Dynamic Programming , DP)是运筹学的一个分支,是求解决策过程最优化的过程。

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都 对应于一个值,我们希望找到具有最优值的解。其基本思想是将待求解问题分解成若干个子问题,先求解 子问题,然后从这些子问题的解得到原问题的解。

与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。如果我们能 够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省 时间 。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算 过,就将其结果填入表中。这就是动态规划法的基本思路。

应用程序性能调优—并发编程(一)

并发编程常用设计模式

(1) MasterWorker模式

其核心思想是由两类线程(Master线程和Worker线程)协作工作。其中Master线程主要负责接收和分 配任务; Worker线程负责处理子任务。 Master线程最终会归纳汇总Worker线程的工作结果。

(2)保护性暂停模式

其主要思想是服务端准备好时才提供服务。假设有一种高并发场景,服务端在短时间内会收到大量请求。 为了不丢弃请求并且保护服务端,可以用一个队列来做削峰,由服务端工作线程从队列里获取请求后逐一 处理。

(3)不变模式

为了尽可能的去除由于线程安全而引发的同步操作,提高并行性能,尽量使用不可变对象。

(4)生产者—消费者模式

有若干个生产者线程和消费者线程。生产者负责提交任务请求,消费者负责处理任务请求。它们可以通 过共享内存缓存区进行通讯。

(5) Future模式

先返回一个包装对象,异步装载真实数据。主线程可先处理其它逻辑,当需要用到时再去获取装载的真 实数据。

并发集合类

(1)列表

Vector. CopyOnWriteArrayList. 读多写少适合用CopyOnWriteArrayList, 并发写多适合用Vector.

(2)集合

SynchronizedSet. CopyOnWriteSet, 读多写少适合用CopyOnWriteSet, 并发写多适合用

SynchronizedSet.

(3) Map

ConcurrentHashMap , SynchronizedHashMap. ConcurrentHashMap使用的是分段锁,性能上较好。

(4)队列

ConcurrentLinkedQueue , BlockingQueue. ConcurrentLinkedQueue通过无锁方式提供了高性能的无 界线程安全队列。 BlockingQueue为阻塞队列,适用于生产者消费者模式,可自动平衡两边的处理速度。

并发控制方法

(1)volatile

被该关键字修饰的变量,如被修改,能确保被其它线程可见。并且会保证其有序性,禁止指令重排序。

(2) Synchronized

同步锁,可修饰方法、代码块等。

(3)ReadWriteLock

可以有多个线程同时获取读锁;但只能有一个线程获取写锁。适用于读多写少的场景。

(4)ReentrantLock

相比Synchronized,提供更强大的功能,包括设置锁等待时间、快速锁轮询等。

(5)Semaphore

信号量。相当于对锁的扩展。根据信号量大小,一次可以允许多个线程同时访问

(6)ThreadLocal

为每个线程提供一个变量副本,保证线程安全。但这并不是一种线程间共享的解决方案。

代码层面对锁优化

(1)避免死锁

指定锁顺序;尽量使用定时锁;尽量减少锁持有时间、减少锁代码块范围等。

(2)减小锁粒度

如ConcurrentHashMap采用分段锁机制。

(3)尽量用读写锁代替独占锁

(4)考虑锁分离

如LinkedBlockingQueue用两把锁(putLock , takeLock) 来分离头出尾进操作。

最近发表
标签列表