并发-性能与可伸缩性


影响性能的因素

  1. 性能与可伸缩性

    • 可伸缩性指的是: 当增加计算资源时(如CPU丶内存丶存储容量或I/O带宽),程序的吞吐量或处理能力相应地增加
  2. 线程引入的开销

    • 在多线程的调度和协调过程中都需要一定的性能开销: 对于为了提升性能而引入的线程来说,并行带来的性能提升必须超过并发导致的开销
  3. 上下文切换

    • 如果主线程是唯一线程,那么它基本上不会被调度出去.另一方面,如果可运行的线程数大于CPU的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其他线程能够使用CPU,这将导致一次上下文切换,切换上下文需要一定的开销(JVM和操作系统的开销丶缓存缺失等)
    • 在程序中发生越多的阻塞,与CPU密集型的程序就会发生越多的上下文切换,从而增加调度开销(无阻塞算法可以有助于减少上下文切换)
    • 上下文切换实际开销跟平台有关,大多数通用处理器上下文切换开销相当于5000 ~ 10000个时钟周期,约几微秒
  4. 内存同步

    • 同步操作的性能开销包括多个方面,synchronized与volatile提供的可见性保证中可能会使用一些特殊指令,即内存栅栏(Memory Barrier),内存栅栏可以刷新缓存,使缓存无效,而且在内存栅栏中,大多数操作都不能重排序
  5. 阻塞

    • 在锁上发生竞争时,竞争失败的线程肯定会阻塞,JVM在实现阻塞行为时,可以采用自旋等待(时间短)(Spin-Waiting)或者通过操作系统挂机被阻塞的线程(时间长),效率高低取决于上下文切换的开销以及在成功获取锁之前需要等待的时间
    • 在运行和阻塞之间切换,就相当于一次上下文切换

      提升性能的方式

  6. 减少锁的竞争

    • 串行操作会降低可伸缩性,上下文切换也会降低性能,在锁上发生竞争时同样导致这两个问题,因此要考虑减少锁的竞争
    • 在并发程序中,对可伸缩性最主要的威胁就是独占方式的资源锁
    • 三种方式减少锁竞争: 1.减少锁的持有时间; 2.降低锁的请求频率; 3.使用带有协调机制的独占锁,这些机制允许更高的并发性
  7. 缩小锁的范围(快进快出)

    • 降低发生锁竞争的可能性的一种有效方式就是尽可能的缩短锁的持有时间
    • 尽管缩小同步代码块能提高可伸缩性,但同步代码块也不能过小,一些需要采用原子方式执行的操作,必须包含在一个同步块中
  8. 减小锁的粒度

    • 降低线程请求锁的频率(从而减少发生竞争的可能性)也属于减少锁的持有时间,这可以通过锁分解和锁分段等技术来实现
    • 采用多个相互独立的锁来保护独立的状态变量,从而改变这些变量在之前由单个锁来保护的情况,这些技术能减少锁操作的粒度,并能实现更高的可伸缩性
    • 使用锁越多,那么发生死锁的风险也越高
      //锁分解前(使用的都是同一个锁this)
      public class ServerStatus{
        public final Set<String> users;
        public final Set<String> queries;
        public synchronized void addUser(String u){ users.add(u); }
        public synchronized void addQuery(String q){ queries.add(q); }
      }
      //使用锁分解技术(分解为多个锁)
      public class ServerStatus{
        public final Set<String> users;
        public final Set<String> queries;
        public void addUser(String u){
            synchronized(users){ users.add(u); }
        }
        public void addQuery(String q){
            synchronized(queries){ queries.add(q); }
        }
      }
  9. 锁分段

    • 将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种情况被称为锁分段
    • 在ConcurrentHashMap(JDK1.7以下)中使用了一个包含16个锁的数组,每个锁保护整个散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护,当需要put元素时,并不对整个hashmap进行加锁,而是先通过hashcode来知道它要放到哪一个分段中,然后对这个分段进行加锁,所以多线程put时,只要不是放到同一个分段中,就实现了并行插入
    • 锁分段的一个劣势在于: 与采用单个锁来实现独占访问相比,获取多个锁来实现独占访问(某些情况下需要加锁整个容器)将更加困难,并且开销更高
  10. 避免热点域

    • 将一些反复计算的结果缓存起来,会引入一些热点域(Hot Field),而这些热点域会限制可伸缩性
  11. 一些替代独占锁的方法

    • 放弃独占锁,使用一种友好并发的方式来管理共享状态,如使用并发容器丶读写锁丶不可变对象及原子变量
  12. 避免使用对象池

    • 在多线程中使用对象池,需要某种同步来协调对象池数据结构的访问,对象池带来的竞争可能形成一个可伸缩性瓶颈,虽然这看似一种性能优化技术,但实际上却会导致可伸缩性问题,对象池有其特定的用途,但对于性能优化来说,用途是有限的
  13. 减少上下文切换

    • 在许多任务中都包含一些可能被阻塞的操作,当任务在运行和阻塞这两个状态之间转换时,就相当于一次上下文切换
    • 减少锁的竞争就能减少阻塞的可能性,同时减少上下文切换,因为在获取锁上发生竞争时将会导致更多的上下文切换

文章作者: Bryson
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Bryson !
评论
 上一篇
并发-显式锁 并发-显式锁
Lock与ReentrantLock Lock接口 Lock提供了一种无条件的丶可轮询的丶可定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的public interface Lock{ void lock(); //阻塞的
2019-08-30
下一篇 
并发-避免活跃性问题 并发-避免活跃性问题
死锁 当一个线程永远的持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞 eg: 1.线程A持有锁L并想获得锁M,同时线程B持有锁M并尝试获取锁L,那么这两个线程将永远地等待下去,这种情况就是最简单的死锁形式,称为抱死(D
2019-08-27
  目录