性能与可伸缩性

一、对性能的思考

  思考: 使用多线程必定能提升程序的性能吗?
  要想经过并发来得到更好的性能,须要作好两件事;更有效的利用现有的处理资源,以及在出现新的处理资源时使程序尽量利用这些资源。缓存

1.1性能与可伸缩性

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

1.2评估各类性能权衡因素

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

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合并为单个锁的获取和释放操做。jvm

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密集
  • 外部限制
  • 锁竞争