第11章,性能与可伸缩性

11.1.2 评估各种性能权衡因素

在大多数性能决策中都包含有多个变量,并且非常依赖于运行环境。在使某个方案比其他方案“更快”之前,首先问自己一些问题:
·“更快”的含义是什么?
·该方法在什么条件下运行得更快?在低负载还是高负载的情况下?大数据集还是小数据集?能否通过测试结果来验证你的答案?
·这些条件在运行环境中的发生频率?能否通过测试结果来验证你的答案?
·在其他不同条件的环境中能否使用这里的代码?
·在实现这种性能提升时需要付出哪些隐含的代价,例如增加开发风险或维护开销?这种权衡是否合适?

~优化性能之前,回答这些问题有很帮助。

11.2 Amdahl定律#

Amdahl定律描述的是:在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中可并行组件与串行组件所占的比重。假定F是必须被串行执行的部分,那么根据Amdahl定律,在包含N个处理器的机器中,最高的加速比为:Speedup <= 1/[1/F+(1-F)/N]。
当N趋近无穷大时,最大的加速比趋近于1/F。因此,如果程序有50%的计算需要串行执行,那么最高的加速比只能是2(而不管有多少个线程可用);如果在程序中有10%的计算需要串行执行,那么最高的加速比将接近10。

~知道加速有限制,这个限制就是串行组件占比。

11.3.2 内存同步

在synchronized和volatile提供的可见性保证中可能会使用一些特殊指令,即内存栅栏(MemoryBarrier)。内存栅栏可以刷新缓存,使缓存无效,刷新硬件的写缓冲,以及停止执行管道。内存栅栏可能同样会对性能带来间接的影响,因为它们将抑制一些编译器优化操作。在内存栅栏中,大多数操作都是不能被重排序的。

~以前也知道内存栅栏,但是不知道内存栅栏能够做什么,而上面的对此进行了说明。

11.4 减少锁的竞争#

有3种方式可以降低锁的竞争程度:
·减少锁的持有时间。
·降低锁的请求频率。
·使用带有协调机制的独占锁,这些机制允许更高的并发性。

~减少锁的持有时间,减小加锁块的范围或大小,将不一些不需要加锁的代码块移到加锁块之外,这样线程持有锁的时间将减少。降低锁的请求频率,比如某一对象多种类型的操作都需要获取同一把锁,现在把它改造成每个类型的操作都有自己的锁,那么每一把锁请求的频率将减低。

11.4.2 减小锁的粒度

另一种减小锁的持有时间的方式是降低线程请求锁的频率(从而减小发生竞争的可能性)。这可以通过锁分解和锁分段等技术来实现,在这些技术中将采用多个相互独立的锁来保护独立的状态变量,从而改变这些变量在之前由单个锁来保护的情况。这些技术能减小锁操作的粒度,并能实现更高的可伸缩性,然而,使用的锁越多,那么发生死锁的风险也就越高。

~降低锁的请求频率,这里提供了锁分解,锁分段两种方式,锁分解就是将一个锁拆分为两个锁,锁分段就是将锁拆分为三个或更多个锁,比如ConcurrentHashMap,就是将初始化一个16个桶,每个桶持有一把锁。

11.4.3 锁分段

锁分段的一个劣势在于:与采用单个锁来实现独占访问相比,要获取多个锁来实现独占访问将更加困难并且开销更高。通常,在执行一个操作时最多只需获取一个锁,但在某些情况下需要加锁整个容器,例如当ConcurrentHashMap需要扩展映射范围,以及重新计算键值的散列值要分布到更大的桶集合中时,就需要获取分段所集合中所有的锁。

~也就是说在锁分段的情况下,只要持有单个锁的操作,那么性能将得到提升,而需要持有整个分段锁的操作,性能会下降。

11.4.5 一些替代独占锁的方法

第三种降低竞争锁的影响的技术就是放弃使用独占锁,从而有助于使用一种友好并发的方式来管理共享状态。例如,使用并发容器、读-写锁、不可变对象以及原子变量。 原子变量提供了一种方式来降低更新“热点域”时的开销,例如静态计数器、序列发生器、或者对链表数据结构中头节点的引用。原子变量类提供了在整数或者对象引用上的细粒度原子操作(因此可伸缩性更高),并使用了现代处理器中提供的底层并发原语(例如比较并交换[compare-and-swap])。

~代替独占锁的方法其实就是使用并发容器,读写锁,不可变对象,以及原子变量(CAS)