Loading

Java并发之性能和可伸缩性

一.概述

线程的最主要的目的就是提高程序的运行性能。 本章将学习各种分析,监测以及提升并发程序性能的技术。但是我们在提升性能的同时要考虑到:程序的安全性才是第一位的。 我们要首先保证我们的程序在正确的前提下提升性能。 所以我们需要一些专业的知识去分析如何在不破坏程序的正确性的前提下提升性能。 这也是我们学习本章的目的。

二.具体学习

1.对性能的思考
提升性能意味着用更少的“资源”做更多的事情。 这里 “资源” 的含义很广,对于一个给定的操作,通常会缺乏某种特定的资源,例如:CPU时钟周期,内存,网络带宽,I/O带宽,数据库请求,磁盘空间以及其他资源。

1)当操作性能由于某种特定的资源而受到限制时,我们通常将该操作称为资源密集型的操作。 例如:CPU密集型,数据库密集型等。

2)尽管使用多个线程的目标是提升整体性能,但与单线程的方法相比,使用多个线程总会引入一些额外的性能开销。 造成这些性能开销的操作有:线程之间的协调(加锁机制,内存同步机制等),增加的上下文切换,线程的创建和销毁,线程的调度等。
在有些时候如果过度的使用多线程,那么多线程的开销甚至会超过由于提高吞吐量,响应性或者计算能力带来的性能提升。(这时就得不偿失) 由此可见:一个并发设计糟糕的应用程序的性能甚至与实现相同功能的串行程序的性能还要差。

3)要想通过并发来获得更好的性能,我们需要努力做好:更有效的利用现有处理资源和在出现新的处理资源时使程序尽可能地利用这些新资源。

2.性能和可伸缩性
1)性能
应用程序的性能可以采用多个指标来衡量,例如服务时间,延迟时间,吞吐率,效率等。简单来说就是 处理速度处理能力。

2)可伸缩性
可伸缩性是指当增加计算资源时(例如CPU,内存,存储容量或I/O带宽),程序的吞吐量或者处理能力能相应地增加。

3)性能为主和伸缩性为主的差别
在并发应用程序种针对可伸缩性进行设计和调整时所采用的方法与传统的性能调优方法截然不同。

当进行性能调优时,其目的通常是用更小的代价完成相同的工作,例如通过缓存来重用之前计算的结果,或者采用时间复杂度为O(nlogn)的算法来代替复杂度为O(n^2)的算法。

在进行可伸缩性调优时,其目的时设法将问题的计算并行化,从而能利用更多的计算资源来完成更多的工作。

4)我们熟悉的三层程序模型(表现层,业务逻辑层,持久化层)是彼此独立的,并且可能由不同的系统来处理,它很好的说明了提高可伸缩性通常会造成性能损失的原因。 如果把表现层,业务逻辑层和持久化层都融合到单个应用程序中,那么在处理第一个工作单元时,其性能肯定要高于将应用程序分为多层并将不同层次分布到多个系统时的性能。这种单一的应用程序避免了在不同层次之间传递任务时存在的网络延迟,同时也不需要将计算过程分解到不同的抽象层次,因此可以减少许多开销。
然而,但是,当这种单一的系统到达自身处理能力的极限时,会遇到一个严重的问题:要进一步提升它的处理能力将非常困难。 所以我们通常会接受每个工作单元执行更长的时间或消耗更多的计算资源,以换取应用程序在增加更多资源的情况下处理更高的负载。

对于服务器应用程序来说,“多少” 这个方面------可伸缩性,吞吐量和生产量,往往比 “多快” 这个方面更受重视。

3.评估各种性能权衡因素
所有的工程决策中都会涉及某些形式的权衡,在建设桥梁时,使用更粗的钢筋可以提高桥的负载能力和安全性,但同时也会提高建造成本。 类似,在软件工程中也会做相应的权衡,例如 快速排序 算法 在大规模数据集上的执行效率非常高,但对于小规模的数据集来说,冒泡排序貌似更加高效。 如果要实现一个高效的排序算法,那么就需要知道我们要处理的数据集的大小,还有衡量优化的指标,包括:平均计算时间,最差时间,可预知性。 然而,编写某个库中排序算法的开发人员通常无法获得这些需求信息。 这就是为什么大多数优化措施都不成熟的原因之一:他们无法获得一组明确的需求。

注意:避免不成熟的优化,首先使程序正确,然后再提高运行速度—如果它还运行的不够快。

很多性能优化措施通常都是以牺牲或者可维护性为代价 (代码越聪明,就越难以理解和维护)。有时候,优化措施会破坏面向对象的设计原则,例如需要打破封装,有时候它们又会带来更高的错误风险,因为通常越快的算法就越复杂。(如果我们无法找出其中的代价或风险,那么或许还没有对这些优化措施进行彻底的思考和分析。)

在大多数性能决策中都包含有多个变量,并且非常依赖于运行环境。在使某个方案比其他方案 “更快” 之前,首先问自己一些问题:

“更快” 的含义是什么?
该方法在什么条件下运行得更快? 在低负载还是高负载的情况下? 大数据集还是小数据集? 能否通过测试结果来验证你得答案?
这些条件在运行环境中的发生频率? 能否通过测试结果来验证你的答案?
在其他不同条件的环境中能否使用这里的代码?
在实现这种性能提升时需要付出哪些隐含的代价,例如增加开发风险或维护开销? 这种权衡是否合适?

在进行任何与性能相关的决策时,都应该考虑上面的这些问题,我们为什么要推荐这种保守的优化方法?因为对性能的提升可能是并发错误的最大来源。有人认为同步机制 “太慢” ,因而采用一些看似聪明实则危险的方法来减少同步的使用,这也通常作为不遵守同步规则的一个常见借口,然而,由于并发错误是最难追踪和消除的错误,因此对于任何可能会引入这类错误的措施,我们都需要谨慎!!

注意:并发有风险,优化需谨慎。

在对性能的调优时,一定要有明确的性能需求(这样才能知道什么时候需要调优以及什么时候应该停止),此外还需要一个测试程序以及真实的配置和负载等环境。

注意:在对性能优化后,我们需要再此测量以验证是否到达了预期的性能提升目标。 已测试为基准,不要猜测。

4.Amdahl定律
在有些问题里,如果可用资源越多,那么问题的解决速度就越快。 例如参与收割庄稼的工人越多,那么就能越快地完成收割工作。 但是有些任务的本质上是串行的,例如即使增加再多的工人也无法增加作物的生长速度。

所以:如果使用线程主要是为了发挥多个处理器的处理能力,那么就必须对问题进行合理的并行分解,并使得程序能有效地使用这种潜在地并行能力

大多数并发程序都与农业耕作有着许多相似之处,它们都是由一系列地并行工作和串行工作组成的。
Amdahl定律描述的是:在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中可并行组件与串行组件所占的比重。

我们假设F是必须被串行执行的部分,那么根据Amdahl定律,在包含N个处理器的机器中,最高加速比为:
Speedup<= 1/{F+(1-F)/N}

当N趋近于无穷大时,加速比大,为1/F。 因此程序有50%的计算需要串行执行时,那么最高加速比为2,实际的加速比肯定小于2的。

1)其实很多时候,我们都不能避免串行部分,例如:

public class WorkerThread extends Thread{
   
  private final BlockingQueue<Runnable> queue;

  public WorkerThread(BlockingQueue<Runnable> queue){
   
    this.queue=queue;
  }
  public void run(){
   
    while(true){
   
      try{
   
        Runnable task = queue.take();//串行部分
        task.run();
      }catch(InterruptedException e)
posted @ 2020-12-13 13:21  文牧之  阅读(27)  评论(0)    收藏  举报  来源