性能与可伸缩性

398 阅读6分钟

1、对性能的思考

  思考: 使用多线程一定能提高程序的性能吗?
  要想通过并发来获得更好的性能,需要做好两件事;更有效的利用现有的处理资源,以及在出现新的处理资源时使程序尽可能利用这些资源。

1.1性能与可伸缩性

  应用程序的性能可以采用多个指标来衡量,例如服务时间、延迟时间、吞吐率、效率、可伸缩性以及容量,有衡量运行速度的,即多快能完成,又衡量处理能力的,能完成多少工作。多快和多少是相互独立的,有时候是相互矛盾的。如程序的三层模型,把表现层、业务层和持久层融合到一个单元。性能肯定要高于将应用程序分为多层并将不同层次分不到多个系统时的性能。然而,当单一系统达到自身处理能力极限时,再进一步提升处理能力将会很困难。
  可伸缩性是指当增加计算资源(cpu、内存、存储容量或者I/O带宽)时,程序的吞吐量或者处理能力能相应的增加。
  通常会接受执行更长的时间或消耗更多的计算资源,以换取应用程序能处理更高的负载。

1.2评估各种性能权衡因素

性能优化的前提要保证程序的正确运行。大多数性能决策中都包含有多个变量, 有时候会增加某种形式的成本来降低另一种形式的开销。对优化措施要进行彻底的思考和分析,因为对性能的提升可能是并发错误最大的来源。对性能调优时,要明确需求,以测试为基准。

2.Amdahl定律

在增加计算资源的情况下,程序在理论上能够实现的最高加速比,取决于程序中可并行组件与串行组件所占的比重。F是串行部分,在包含N个处理器的机器中最高加速比为:


当N趋近于无穷大时,最大的加速比趋近于1/F。从下图可以看出串行部分对吞吐率的影响。

3.线程引入的开销

3.1 上下文切换

  如果可运行的线程数大于cpu的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其他线程能够使用cpu, 这就导致一次上下文切换。上下文切换存在一定的开销,在调度过程中需要访问有操作系统和JVM共享的数据结构cpu时钟周期,jvm和操作系统消耗的cpu周期越多,应用程序可用的越少。当一个新的线程被切换进来,他所需要的数据可能不在当前处理器的本地缓存中,这导致更加缓慢。
  上下文切换频繁,将降低吞吐量,unix系统vmstat命令可查看上下文切换次数及内核中执行时间所占的比例等。如果内核占用超过10%,很可能是I/O或竞争锁导致的阻塞。

3.2内存同步

  synchronized和volatile在可见性保证中将抑制编译器的优化操作,禁止指令重排序。不论是有竞争同步还是无竞争同步将会消耗一部分的cpu的时钟周期。现代jvm能通过优化去掉一些不会发生竞争的锁,因此我们要将重点放在锁竞争的地方。

     synchronized (new Object()) {
         //...
     }

  一些完备的JVM能够通过逸出分析去掉锁。编译器也可以执行锁力度的粗化操作。如果引用是线程本地的,下面的程序通过逸出分析将去掉四次锁获取操作。编译器会把三个add和1个toString合并为单个锁的获取和释放操作。

     public String getStoogeNames (){
         List<String> stooges = new vector<String>();
         stooges.add("Moe");
         stooges.add("Larry");
         stooges.add("Curly");
         return stooges.toString();
     }    

3.3 阻塞

  对于竞争的同步,jvm在实现阻塞的时候可以采用自旋等待和线程挂起两种方式,如果等待时间较长就线程挂起,如果时间短就自旋。对自旋的优化是自适应自旋,根据历史等待时间确定是否自旋,现在大多数是采用线程挂起。阻塞时将包含两次额外的上下文切换。

4 减少锁的竞争

有三种方式可以降低锁的竞争程度:

  • 减少锁的持有时间
  • 降低锁的请求频率
  • 使用带有协调机制的独占锁

4.1 缩小锁的范围

尽可能缩短锁的持有时间,可以将与锁无关的代码移出同步代码块,比如将synchronized方法改为synchronized代码块。

4.2 减小锁的粒度

  如果一个锁需要保护多个相互独立的状态变量,可以将这个锁分解为多个锁,并且每个锁只保护一个变量,从而降低每个锁被请求的频率。锁分解是把竞争的锁转化为非竞争的锁,从而提高性能和可伸缩性

     public class ServerStatus {
         public final Set<String> users;
         public final Set<String> users;
         ...
         public synchronized void addUser(String u){users.add(u);}
         public synchronized void addQuery(String q) {queries.add(q);}
         public synchronized void removeUser(String u){
             users.remove(u);
         }
         public synchronized void removeQuery(String q){
             queries.remove(q);
         }
     }

将ServerStatus重新改写为使用锁分解技术:

     public class ServerStatus {
         public final Set<String> users;
         public final Set<String> users;
         ...
         public void addUser(String u){
             synchronized (users) {
                 users.add(u)
             }
         }
         public void addQuery(String q) {
             synchronized (queries) {
                 queries.add(q);
             }
         }
}

4.3 锁分段

将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种情况称为锁分段。比如ConcurrentHashMap。

4.4 避免热点域

每个操作都请求多个变量时,锁的粒度将很难降低,将一些结果缓存起来会引入热点域。比如hashmap中size方法,每个元素的变化操作都需要访问它。currenthashmap就进行了相应的优化,不是维护一个全局的技术,而是将每个分段中的数量相加。

4.5 替代独占锁

另一种降低竞争锁影响的技术就是放弃使用独占锁, 可以使用ReadWriteLock, 原子变量。

4.6 监测cpu使用率

当测试伸缩性的时候,要保证处理器得到充分的使用。如果cpu没有充分利用,通常有一下原因:

  • 负载不充足
  • I/O密集
  • 外部限制
  • 锁竞争