zoukankan      html  css  js  c++  java
  • 并发编程学习笔记之可伸缩性(九)

    很多改进性能的技术增加了复杂度,因此增加了安全和活跃度失败的可能性.

    更糟糕的是,有些技术的目的是改善性能,事实上产生了相反的作用,带来了其他的性能问题.

    数据的正确性永远是第一位的,保证程序是正确的,然后再让它更快.只有当你的性能需求和评估标准需要程序运行得更快时,才去进行改进.

    在设计并发应用程序的时候,最大可能地改进性能,通常并不是最重要的事情.

    性能的思考

    当活动的运行因某个特定资源受阻时,我们称之为受限于该资源:受限于CPU,受限于数据库.

    使用线程的目的是希望全面提升性能,但是与单线程相比,使用多线程会引入一些额外的开销.

    如:

    • 协调线程相关的开销(加锁、信号、内存同步)
    • 增加的上下文切换
    • 线程的创建和消亡,以及调度的开销

    当线程被过度使用后,这些开销会超过提高后的吞吐量响应性和计算能力带来的补偿.

    一个没能经过良好并发设计的应用程序,甚至比相同功能的顺序的程序性能更差.

    性能"遭遇"可伸缩性

    可伸缩性指的是:当增加计算资源的时候(比如增加额外CPU数量、内存、存储器、I/O带宽),吞吐量和生产量能够相应地得以改进.

    对性能的权衡进行评估

    避免不成熟的优化,首先使程序正确,然后再加快----如果它运行得还不够快.
    

    很多性能的优化会损害可读性或可维护性--代码越"聪明",越"晦涩",就越难理解和维护.

    在多个方案之间进行选择的时候,先问自己一些问题:

    • 你所谓的更"快"指的是什么
    • 在什么样的条件下你的方案能够真正运行得更快?在轻负载还是重负载下?大数据集还是小数据集?是否支持你的测量标准答案?
    • 这些条件在你的环境中发生的频率?是否支持你的测量标准的答案?
    • 这些代码在其他环境的不同条件下被用到的可能性?
    • 你用什么样隐含的代价,比如增加的开发风险或维护性,换取了性能的提高?这个权衡的决定是否正确?

    做出任何与性能相关的工程决定时,都应该考虑这些问题.

    最好选择保守的优化方案,因为对性能的追求很可能是并发bug唯一最大的来源.通过减少同步来提高响应性,成了不遵守同步规定的常用的借口,但是因为并发bug是最难追踪和消除的,所以任何引入这类bug的行动风险都需要慎重进行.

    优化改进后的代码,一定要进行压力测试.主观认为会提高性能的代码,在实际生产环境可能会出现问题.

    测评,不要臆测
    

    Amdahl 定律

    Amdahl定律描述了在一个系统中,基于可并行化和串行化的组件各自所占的比重,程序通过获得额外的计算资源,理论上能够加速多少.

    如果F是必须串行化执行的比重,那么Amdahl定律告诉我们,在一个N处理器的机器中,我们最多可以加速:

    image

    串行执行的比率越大,处理器越多,处理器的利用率越低:

    image

    线程引入的开销

    调度和线程内部的协调都要付出性能的开销: 对于改进性能的线程来说,并行带来的性能优势必须超过并发所引入的开销.

    切换上下文

    如果可运行的线程大于CPU的数量,那么操作系统最终会强行换出正在执行的线程,从而使其他线程能够使用CPU,这回引起上下文切换,他会保存当前运行线程的执行上下文,并重建新调入线程的执行上下文.

    切换上下文会有资源的损耗.

    一个程序发生越多的阻塞(阻塞I/O,等待竞争锁,或者等待条件变量),与受限于CPU的程序相比,就会造成越多的上下文切换,这增加了调度的开销,并减少了吞吐量(无阻塞的算法可以减少上下文切换).

    Unix系统的vmstat命令和Windows系统的perfmon工具都能报告上下文切换次数和内核占用的时间等信息.

    阻塞

    多个线程竞争加锁的方法的时候,失败的线程必然发生阻塞.

    JVM在阻塞的时候有两种处理方式:

    • 自旋等待(spin-waiting,不断尝试获取锁,直到成功).
    • 挂起(suspending)这个阻塞的线程.

    自旋等待适合短期的等待.挂起适合长期间等待.,有一些JVM基于过去等待时间的数据剖析来在这两者之间选择,但是大多数等待锁的线程都是被挂起的.

    减少锁的竞争

    串行化会损害可伸缩性,上下文切换会损害性能.竞争性的锁会同时导致这两种损失,所以减少锁的竞争能够改进性能和可伸缩性.

    访问独占锁守护的资源是串行的--一次只能有一个线程访问它.使用锁可以避免过期数据,但是安全性是用很大的代价换来的,对锁长期的竞争会限制可伸缩性.

    并发程序中,对可伸缩性首要的威胁是独占的资源锁.
    

    有两个原因影响着锁的竞争性:

    • 锁被请求的频率
    • 每次持有锁的时间

    如果这两者的乘积足够小,那么大多数请求锁的尝试都是非竞争的,这样竞争性的锁将不会成为可伸缩性巨大的障碍.

    但是,如果这个锁的请求量很大,线程将会阻塞以等待锁.在极端的情况下,处理器将会闲置,即使仍有大量工作等待着完成.

    有三种方式来减少锁的竞争:

    • 减少持有锁的时间;
    • 减少请求锁的频率;
    • 或者用协调机制取代独占锁,从而允许更强的并发性.

    缩小锁的范围("快进快出")

    减少竞争发生可能性的有效方式是尽可能缩短把持锁的时间.尽量缩小synchronized代码块,尤其是那些耗时的操作,以及那些潜在的阻塞操作(I/O).

    减少锁的粒度

    减少持有锁的时间比例的另一种方式是让线程减少调用它的频率(因此减少发生竞争的可能性).

    可以通过使用分拆锁(lock splitting)和分离锁(lock striping)来实现,也就是采用相互独立的锁,守卫多个独立的状态变量,在改变之前,它们都是由一个锁守护的.这些技术减少了锁发生时的粒度,潜在实现了更好的可伸缩性---但是使用更多的锁同样会增加死锁的风险.

    如果一个锁 守卫数量大于一、且相互独立的状态变量,你可能能通过分拆锁,使每一个锁守护不同的变量,从而改进可伸缩性.结果是每个锁被请求的频率都减少 了.

    使用相同的锁:

    public class NewLock {
        //对象A
        private final Object objA = new Object();
        //队相比
        private final Object objB = new Object();
    
        public synchronized Object getObjA(){
                return objA;
        }
    
        public synchronized Object getObjB(){
                return objB;
        }
    
    }
    

    使用不同的锁(分拆锁),减少了锁的请求频率:

    public class NewLock {
        //对象A
        private final Object objA = new Object();
        //队相比
        private final Object objB = new Object();
    
        public Object getObjA(){
            synchronized (objA){
                return objA;
            }
        }
    
        public Object getObjB(){
            synchronized (objB){
                return objB;
            }
        }
    
    }
    

    分拆锁对于竞争并不激烈的锁,能够在性能和吞吐量方面产生一些纯粹的改进,尽管这可能会在性能开始因为竞争而退化时增加负载的极限.

    分拆锁对于中等竞争强度的锁,能够切实地把它们大部分转化成非竞争的锁,这个结果是性能和可伸缩性都期望得到的.

    分离锁

    分拆锁对性能的改进有一些局限性,不能大幅地提高多个处理器在同一系统中并发性的能力.

    分拆锁有时候可以被扩展,分成可大可小加锁块的集合,并且它们归属于相互独立的对象,这样的情况就是分离锁.

    分离锁的一个负面作用是:对容器加锁,进行独占访问更加困难,并且更加昂贵了.

    分拆锁和分离锁能够改进可伸缩性,因为它们能够使不同的线程操作不同的数据(或者相同数据结构的不同部分),而不会发生相互干扰.

    能够从分拆锁收益的程序,通常是那些对锁的竞争普遍大于对锁守护数据竞争的程序.

    例如: 一个锁守护两个独立变量X和Y,线程A想要访问X,而线程B想要访问Y,这两个线程没有竞争任何数据,然而它们竞争相同的锁.

    独占锁的替代方法

    用于减轻竞争锁带来的影响的第三种技术是提前使用独占锁,这有助于使用更友好的并发方式进行共享状态的管理.

    这包括:

    • 使用并发容器
    • 读-写锁
    • 不可变对象
    • 原子变量

    读写锁

    读写锁实行了一个多读者-单写者(multiple-reader,single-write)加锁规则:只要没有改变,多个读者可以并发访问共享资源,但是写者必须独占获得锁.

    对于多数操作都为读操作的数据结构,ReadWriteLock与独占的锁相比,可以提供更好的并发性.

    对于只读的数据结构,不变性可以完全消除加锁的必要.

    原子变量

    原子变量类提供了针对整数或对象引用的非常精妙的原子操作,因此更具可伸缩性.

    如果你的类只有少量热点域(例如:多个方法都在调用的计数操作,就是一个热点域),并且该类不参与其它变量的不变约束,那么使用原子变量替代它可能会提高可伸缩性.

    检测CPU利用率

    当我们测试可伸缩性的时候,我们的目标通常是保持处理器的充分利用.

    Unix系统的vmstat和mpstat,或者Windows系统的perfmon都能够告诉你处理器有多忙碌.

    如果所有的CPU都没有被均匀地利用(有时CPU很忙碌地运行,有时很清闲),那么你的首要目标应该是增强你程序的并行性.

    不均匀的利用率表名,大多数计算都有很小的线程集完成,你的应用程序将不能够利用额外的处理器资源.

    如果你的CPU没有完全利用,你需要找出原因.有以下几种:

    • 不充足的负载. 数据量不够多
    • I/O限制
    • 外部限制.可能你的应用程序取决于外部服务,比如数据库或者Web Service 那么瓶颈可能不在于你自己的代码.
    • 锁竞争. 使用Profiling工具能够告诉你,程序中存在多少个锁的竞争,哪些锁很"抢手".或者使用线程转储,如果线程因等待锁被阻塞,与线程转储的栈框架会声明"waiting to lock monitor...".非竞争的锁几乎不会出现在线程转储中:竞争激烈的锁几乎总会只要有一个线程在等待获得它,所以会频繁出现在线程转储中.

    向"对象池"说"不"

    不要使用对象池,对象池跟线程池差不多,为了减少创建和销毁对象的开销,能够重复使用对象,创建了一个对象池,但是现代的JVM对象的分配和垃圾回收已经非常快了.

    如果使用对象池,那么线程从池中请求对象,协调访问池的数据结构的同步就成为必然了,这便产生了线程阻塞的可能性.

    又因为由锁的竞争产生的阻塞,其代价比直接分配的代价多几百倍,即使是很小的池竞争都会造成可伸缩性的瓶颈(甚至是非竞争的同步,其代价也会比分配一个对象大很多).

    所以使用对象池有点得不偿失了,反而效率更低.

    比较Map的性能

    单线程的时候ConcurrentHashMap的性能要比同步的HashMap的性能稍好一点,但是在并发应用中,这种作用就十分明显了.

    ConcurrentHashMap对get操作做了一些优化,提供最好的性能和并发性.

    同步的Map对所用的操作用的都是一个锁,所以同一时刻只有一个线程能够访问map.

    而ConcurrentHashMap并没有对成功的读操作加锁,只对写操作和真正需要锁的读操作使用了分离锁的方法.因此多线程能够并发地访问Map而不被阻塞.

    image

    随着线程数的增加,并发的map吞吐量得到增长.看ConcurrentHashMap在线程数到达16的时候,它的吞吐量不在提高,因为它的内部使用的是16个分离锁的数组,可以支持16个线程同时写,当线程多余这个数量的时候,就得不到提升了(可以增加锁的数量,提高并行性)

    再看同步容器,线程数越多,反而吞吐量降低.

    在对锁的竞争小的境况下,每个操作花费的时间取决于真正工作的时间,吞吐量会因为线程数的增加而增加.

    一旦竞争变得激烈,每个操作花费的时间就由上下文切换和调度延迟决定了,并且加入更多的线程不会对吞吐量有什么帮助.

    总结

    • Amdahl定律告诉我们,程序的可伸缩性是由必须连续执行的代码比例决定的.
    • Java程序中串行化首要的来源是独占的资源锁,所以可伸缩性通常可以通过以下这些方式提升:
    1. 减少获取锁的时间
    2. 减少锁的粒度
    3. 减少锁的占用时间
    4. 用非独占或非阻塞锁来取代独占锁
  • 相关阅读:
    CompoundButton.OnCheckedChangeListener与RadioGroup.OnCheckedChangeListener冲突
    C# String.Format格式化json字符串中包含"{" "}"报错问题
    在IHttpHandler中获取session
    你真的会玩SQL吗?删除重复数据且只保留一条
    activity结束之后刷新之前的activity的内容
    jQuery打造智能提示插件二(可编辑下拉框)
    byte数组转float 以及byte转其他类型时为什么要&0xff
    为什么byte的取值范围是-128到127
    MySQL修改表、字段、库的字符集及字符集说明
    MySQL分布式jdbc连接
  • 原文地址:https://www.cnblogs.com/xisuo/p/9869188.html
Copyright © 2011-2022 走看看