我们使用多线程的目的是提升程序的整体性能,但是与单线程的方法相比,使用多个线程总会引入一些额外的性能开销。造成这些开销的操作包括:线程之间的协调(如加锁、内存同步等),增加上下文切换,线程的创建和销毁,以及线程的调度等。如果我们多度地使用线程,那么这些开销可能超过由于提高吞吐量、响应性或者计算能力所带来的性能提升。另一方面,一个并发设计很糟糕的程序,其性能甚至比完成相同功能的串行程序性能还要低。
想要通过并发来获得更好的性能就需要做到:更有效地利用现有处理资源,以及在出现新的处理资源时使程序尽可能地利用这些新资源。
下面我们将介绍如何评估性能、分析多线程带来的额外开销以及如何减少这些开销。
性能和可伸缩性应用程序的性能可以采用多个指标来衡量,例如服务时间、延迟时间、吞吐量、效率、可伸缩性以及容量等。其中一些指标(服务时间、等待时间)用于衡量程序的“运行速度”,即某个指定的任务单元需要“多快”才能处理完成。另一些指标(生产量、吞吐量)用于程序的“处理能力”,即在计算资源一定的情况下,能完成“多少”工作。
可伸缩性指的是:当增加计算资源(例如CPU、内存、存储容量或者I/O带宽)时,程序的吞吐量或者处理能力相应地增加。在对可伸缩性调优时,目的是将设法将问题的计算并行化,从而能够利用更多的计算资源来完成更多的任务。而我们传统的对性能调优,目的是用更小的代价完成相同的工作,例如通过缓存来重用之前的计算结果。
Amdahl定律大多数的并发程序都是由一系列的并行工作和串行工作组成。
Amdahl定律描述的是:在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中可并行组件与串行组件所占比重。简单点说,Amdahl定律代表了处理器并行运算之后效率提升的能力。
假定F是必须被串行执行的部分,那么根据Amdahl定律,在包含N个处理器的机器中,最高加速比为:
\[Speedup <= \frac{1}{F+\frac{(1-F)}{N}}\]
当N趋近于无穷大时,最高加速比趋近于\(\frac{1}{F}\) 。因此,如果程序有50%的计算需要串行执行,那么最高加速比只能是2,而不管有多个线程可用。无论我们采用什么技术,最高也就只能提升2倍的性能。
Amdahl定律量化了串行化的效率开销。在拥有10个处理器的系统中,如果程序中有10%的部分需要串行执行,那么最高加速比为5.3(53%的使用率),在拥有100个处理器的系统中,加速比可以达到9.2(92%的使用率)。但是拥有无限多的处理器,加速比也不会到达10。
如果能准确估计出执行过程中穿行部分所占的比例,那么Amdahl定律就可以量化当有更多计算资源可用时的加速比。
线程引入的开销在多个线程的调度和协调过程中都需要一定的性能开销。所以我们要保证,并行带来的性能提升必须超过并发导致的开销,不然这就是一个失败的并发设计。下面介绍并发带来的开销。
上下文切换
如果主线程是唯一的线程,那么它基本上不会被调度出去。如果可运行的线程数目大于CPU的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其他线程能够使用CPU。这将导致一次上下文切换,在这个过程中,将保存当前运行线程的执行上下文,并将新调度进来的线程的执行上下文设置为当前上下文。
切换上下文需要一定的开销,而在线程调度过程中需要访问由操作系统和JVM共享的数据结构。上下文切换的开销不止包含JVM和操作系统的开销。当一个新的线程被切换进来时,它所需要的数据可能不在当前处理器的本地缓存中,因此上下文切换将导致一些缓存缺失(丢失局部性),因而线程在首次调度运行时会更加缓慢。
调度器会为每个可运行的线程分配一个最小执行时间,即使有许多其他的线程正在等待执行:这是为了将上下文切换的开销分摊到更多不会中断的执行时间上,从而提高整体的吞吐量(以损失响应性为代价)。
当线程被频繁的阻塞时,也可能会导致上下文切换,从而增加调度开销,降低吞吐量。因为,当线程由于没有竞争到锁而被阻塞时,JVM通常会将这个线程挂起,并允许它被交换出去。
上下文切换的实际开销会随着平台的不同而变化,按照经验来看:在大多数通用的处理器上,上下文切换的开销相当于5000~10000个时钟周期,也就是几微秒。
内存同步