多线程性能与可伸缩性
1 单线程程序不需要线程调度也不需要同步开销,而且不需要使用锁来保证数据结构的一致性。在多线程的调度和协调中都需要一定的性能开销。对于为了性能而引入多线程的程序,并行带来的性能提升必须要超过并发导致的开销。
2 上下文切换
如果可运行的线程数大于 CPU 的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其他线程能够使用 CPU。这将导致一次上下文切换,在这个过程中将保存当前运行线程的执行上下文,并将新调度进来的线程执行上下文设置为当前上下文。
切换上下文的开销:线程调度过程中需要访问由操作系统和JVM 共享的数据结构。应用程序、操作系统和JVM都是用一组相同的CPU。jvm 和 操作系统使用越多的cpu时钟周期,应用程序可用的 cpu 周期就越少。而线程切换开销不仅包含 jvm 和 操作系统开销,当一个线程切换进来后它所需要的数据可能不在当前处理器的本地缓存,因此上下文切换将导致一些缓存的缺失,因而线程在首次调度时会更加缓慢。
当线程由于等待某个发生竞争的锁而被阻塞时,JVM 通常会将这个线程挂起,并允许他被交换出去。阻塞包括:阻塞IO、等待获取发生竞争的锁、条件变量上的等待。
如国内和执行时间占用率较高(超过10%),就表示调度活动发生的很频繁,这很可能是 IO 或竞争锁导致的阻塞引起的。
3 内存同步
在评估同步操作带来的性能影响时,区分竞争同步和无竞争同步非常重要。
例如方法的代码片段
public String getnames() { List<String> stooges = new Vector<>(); stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); return stooges.toString(); }
该方法至少会获得释放4次锁。对于 list 的唯一引用就是局部变量 stooges ,并且封闭在栈中的变量都会自动成为线程本地变量。一些智能的运行时编译器会分析这些调用,将去掉这4次无竞争的锁获取操作。
某个线程同步可能会影响其他线程的性能,因为同步会增加共享内存总线上的通信量,总线的带宽是一定的,并且所有处理器都将共享这条总线。如果有多个线程竞争同步总线带宽,那么所有使用同步的线程都会受到影响。
4 在并发程序中,通常更多地将侧重点放在吞吐量和可伸缩性上。
提升程序的可伸缩性方法:
- 减少锁的持有时间
- 降低锁的粒度
- 采用非独占的锁或非阻塞的锁来代替独占锁。
5 评价并行性的指标
吞吐量:指一组并发任务中已完成任务所占的比例
响应性:指请求从发出到完成之间的时间
可伸缩性:指在增加更多资源的情况下(通常指CPU),吞吐量的提升情况。
如果觉得有用,想赞助一下请移步赞助页面:赞助一下