• java
  • go
  • 数据库
  • linux
  • 中间件
  • 书
  • 源码
  • 夕拾

  • java
  • go
  • 数据库
  • linux
  • 中间件
  • 书
  • 源码
  • 夕拾

【第三部分】活跃性、性能与测试

目录

  • 目录
  • 活跃性危险
    • 死锁(deadlyembrace)
      • 锁顺序死锁
      • 动态的锁顺序死锁
      • 协作对象之间发生死锁
      • 活跃性问题
      • 资源死锁
      • 线程饥饿死锁
    • 死锁的避免与诊断
      • 支持定时锁
      • 通过线程转存储信息来分析死锁
    • 其他活跃性问题
      • 饥饿
      • 活锁
  • 性能与可伸缩性
    • 性能
      • 伸缩性
      • 性能决策的权衡
      • amdanhl定律
    • 线程引入的开销
      • 上下文切换
      • 内存同步
      • 阻塞
    • 减少锁的竞争
      • 缩小锁的范围-减少锁的持有时间
      • 减少锁的粒度-降低锁请求频率
      • 热点域
      • 代替独占锁的方法
    • 监测cpu利用率
    • 减少上下文切换
  • 并发程序的测试

活跃性危险

死锁(deadlyembrace)

jvm对死锁的处理:当一组java线程发生死锁,这些线程永远不会再使用了.根据线程完成的工作的不同,可能造成应用完全停止,或者某个特定的子系统完全停止,或者性能降低,恢复的唯一方式是重启.并且希望不会再发生死锁.
问题: 线程池中发生死锁有何影响?

锁顺序死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class LeftRightLock{
private final Object left = new Object():
private final Object right = new Object():

public void leftRight(){
synchronized(left){
synchronized(right){
...
}
}
}
public void rightLeft(){
synchronized(right){
synchronized(left){
...
}
}
}
}

发生死锁的原因: 两个线程试图以不同的顺序获得锁.如果按照相同的顺序获取锁,则不会出现循环加锁的依赖性.

动态的锁顺序死锁

1
2
3
4
5
6
7
8
public void transferMony(Account from,Account to,DollarAmount amount){
synchronized(from){
synchronized(to){
from.debit(amount);
to.credit(amount);
}
}
}

发生死锁的原因:存在嵌套的锁获取操作,无法控制锁的顺序.
可通过制定锁的顺序来避免死锁.通过System.identityHashCoe方法指定顺序.

协作对象之间发生死锁

有些获取多个锁的操作,在不同的对象不同方法中,其未必在同一个方法中被获取.

活跃性问题

在持有锁时调用外部方法,将出现活跃性问题.在这个外部方法中,可能获取其它锁(可能会产生死锁),或者阻塞 ,将导致其他线程无法及时获得当前线程持有的锁.

资源死锁

资源池通常采用信号量实现,若一个任务需要连接两个数据库,且请求两个资源不会始终遵守同一顺序,则可能A持有D1等待D2,B持有D2等待D1.

线程饥饿死锁

如果某些任务需要等待其他任务的结果,这些任务往往是产生死锁的主要来源,有界线程池/资源池与相互依赖的任务不能一起使用。

死锁的避免与诊断

如果一个程序必须使用多个锁,那么在设计时必须考虑锁的顺序:尽量减少潜在的加锁交互数量,将获取锁时需要遵循的协议写入文档并始终遵守。

使用细粒度锁的程序中,通过两阶段策略(Two-Part Strategy)连检查代码中的死锁:
找出在什么地方获取多个锁(使这个集合尽量小),对这些实例进行全局分析,从而确保它们在整个程序中获取锁的顺序保持一致.
尽可能使用开放调用,能极大的简化分析过程.如果所有的调用都是开放调用.那么要发现获取多个锁的实例时非常简单,通过代码审查,或者借助自动化的源码分析工具.

支持定时锁

使用Lock类中定时tryLock功能来代替内置锁机制.当使用内置锁时,只要没有获得锁,就会永远等待下去.而显示锁则可以指定超时时间,超过该时间后,tryLock返回false.

通过线程转存储信息来分析死锁

Jvm 可通过线程转存储(Thread Dump)分析死锁.
jstack -pid

其他活跃性问题

死锁是常见活跃性危险,还有其他活跃性危险:饥饿,丢失信号,活锁.

饥饿

线程由于无法访问它所需要的资源而不能继续执行,就发生了饥饿.
引发饥饿最常见的资源是cpu时钟周期.
如果java应用中,对线程的优先级设置不当,或者持有锁时,执行一些无法结束的结构(while(true),或者无限的等待资源),那么也可能导致饥饿.

避免设置线程优先级,这会增加平台依赖性.

活锁

活锁不会阻塞线程,但程序也不会继续执行,线程总会不断尝试,不断失败.

  1. 活锁通常发生在处理事务消息的应用中:如果不能成功处理消息,那么消息处理机制将回滚整个事务,并将其重新放到队列的开头.这种形式的活锁通常是由过度的错误恢复代码造成的,其错误地将不可修复的错误作为可修复的错误.
  2. 多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,发生活锁.(过于礼貌的人在路上互相让路),解决方法:加入随即等待时间.

性能与可伸缩性

性能

性能提升意味着用更少的资源做更多的事情.
尽可能让cpu都处于忙碌状态(有效计算)

伸缩性

可伸缩性: 增加计算资源(cpu,内存,存储容量或者I/O带宽),程序的吞吐量或者处理能力相应地增加.

应用程序的性能指标:服务时间,延迟时间,吞吐率,效率,可伸缩性以及容量.服务时间,延迟时间用于描述程序的”运行速度”,吞吐量及生产量,描述程序”处理能力”

  • 对服务器应用程序来说,可伸缩性,吞吐量和生产量,比”多块”更重要.

性能决策的权衡

避免不成熟的优化,首先使程序正确.然后再提高运行速度-如果需要提高

性能决策中包含多个变量,且依赖运行环境,再决策之前:

  1. “更快”的含义是什么?
  2. 该方法在什么条件运行的更快?低负载还是高负载,大数据集还是小数据集?能否通过测试结果来验证你的答案?
  3. 这些条件在运行环境中的发生频率?能否通过测试结果验证?
  4. 在其他不同条件能否使用/
  5. 实现这种性能提升需要付出那些隐含代价?权衡是否合适.
  • 对性能的调优时,一定要有明确的性能需求
  • 对性能调优后,需要一个测试程序来模拟真实的配置及负载(以测试为基准, 不要猜测).

amdanhl定律

speedDup <= 1/(f+(1-f)/n)

所有并发程序中都包含一些串行部分。

线程引入的开销

上下文切换

应用程序,jvm,操作系统共用同一组cpu,jvm和操作系统代码消耗越多cpu时钟周期,应用程序可用的cpu时钟周期就越少。

上下文切换的开销并不只是jvm和操作系统的开销,当一个新的线程切换进来时,其所需要的数据可能不在当前cpu的缓存中,因此会有些缓存缺失,第一次运行速度更慢。
调度器为每个可运行线程设置最小执行时间(即使有许多其他的线程正在执行),它将上下文切换的开销分摊到更多不会中断的执行时间上,从而提高整体吞吐量。

当线程由于等待某个发生竞争的锁而被阻塞时,jvm会将这个线程挂起,允许它被交换出去。

上下文切换的消耗随着不同平台变化而变化,在多数通用的处理器中,上下文消耗相当于 5000~10000个时钟周期,也就是几微秒。

  • unix的vmstat命令和windows的perfmon工具都能报告上下文切换次数以及在内核中执行时间所占比例信息,如果内核占用率较高(>10%),那么通常表示调度活动发生的比较频繁。很可能时io或竞争锁导致的。

内存同步

同步操作的性能开销包含多个方面。

有竞争的同步与无竞争的同步:

  • 无竞争的同步开销仍然不为0(20~250个时钟周期),不过影响也微乎其微.jvm还会对其进行优化.

阻塞

不同的线程在竞争锁时,失败的一方会发生阻塞,jvm在实现阻塞行为时,可以采用自旋,或者通过操作系统将线程挂起这两种方式.

  • 自旋:适用等待时间较短
  • 挂起:适用等待时间长

大多数jvm还是选择将线程挂起.

当线程(阻塞或者io阻塞)需要被被挂起,在这个过程,会包含两次额外的上下文切换.

减少锁的竞争

串行操作会降低可伸缩性,上下文切换也会降低性能.在锁上发生竞争会同时导致这两个问题.减少锁的竞争能够提高性能和可伸缩性.

  • 并发程序中,对可伸缩性最大的威胁就是对资独占方式的资源锁

影响发生锁竞争的可能性:锁的请求频率 & 锁持有时间

  • little定律(排队理论):在一个稳定的系统中,顾客的平均数量=平均到达率*平均停留时间

降低锁的竞争程度:

  1. 减少锁的持有时间
  2. 降低锁的请求频率
  3. 使用有协调机制的独占锁,这些机制允许更高的并发性.

缩小锁的范围-减少锁的持有时间

锁持有时间过长,会限制可伸缩性.如:某个操作会持有锁2ms,并且所有操作都需要这个锁,那么无论用有多少个空余处理器,吞吐量也不会超过500个操作/s.

jvm会对锁进行优化,同步代码块虽然尽量小,但也不能过小,一些需要原子操作的必须包含在同一代码块中,同步也需要一定的开销,当把一个同步代码块分解成多个同步代码块,jvm在对锁粒度粗化操作,会将分解的代码块重新合并,甚至会导致性能降低.

减少锁的粒度-降低锁请求频率

降低锁请求频率: 可通过锁分解或者锁分段技术实现.这些技术能减小锁的粒度,并且提高可伸缩性,但是死锁风险也越高.

热点域

当每个操作都会请求多个变量时,锁的粒度很难降低(这是在性能与可伸缩性之间相互制衡的一方面).常见的优化方式是将一些反复计算的结果缓存起来,会引入一些热点域(hot filed),这些hod-field往往会影响可伸缩性.

代替独占锁的方法

放弃使用独占锁,用并发容器,读写锁,不可变对象以及原子遍量.

监测cpu利用率

cpu,io等监控参考 linux uptime 平均负载

判断增加cpu是否可提升性能时,可根据 vmstat命令进行判断,有一栏信息是当前处于可运行但没有运行的线程数量.

锁竞争:随机打印一下线程转存储信息,竞争激烈的总会出现 waiting to lock monitor...

减少上下文切换

并发程序的测试

  • 安全性:不发生任何错误行为
  • 活跃性:某个良好的行为终将发生
  • 吞吐量:一组并发任务中已完成任务所占比例
  • 响应性:请求从发出到完成之间的时间
  • 可伸缩性:增加更多资源的情况下(cpu)吞吐量的提升情况
【第四部分】高级主题
一、并发编程的挑战
  1. 1. 目录
  2. 2. 活跃性危险
    1. 2.1. 死锁(deadlyembrace)
      1. 2.1.1. 锁顺序死锁
      2. 2.1.2. 动态的锁顺序死锁
      3. 2.1.3. 协作对象之间发生死锁
      4. 2.1.4. 活跃性问题
      5. 2.1.5. 资源死锁
      6. 2.1.6. 线程饥饿死锁
    2. 2.2. 死锁的避免与诊断
      1. 2.2.1. 支持定时锁
      2. 2.2.2. 通过线程转存储信息来分析死锁
    3. 2.3. 其他活跃性问题
      1. 2.3.1. 饥饿
      2. 2.3.2. 活锁
  3. 3. 性能与可伸缩性
    1. 3.1. 性能
      1. 3.1.1. 伸缩性
      2. 3.1.2. 性能决策的权衡
      3. 3.1.3. amdanhl定律
    2. 3.2. 线程引入的开销
      1. 3.2.1. 上下文切换
      2. 3.2.2. 内存同步
      3. 3.2.3. 阻塞
    3. 3.3. 减少锁的竞争
      1. 3.3.1. 缩小锁的范围-减少锁的持有时间
      2. 3.3.2. 减少锁的粒度-降低锁请求频率
      3. 3.3.3. 热点域
      4. 3.3.4. 代替独占锁的方法
    4. 3.4. 监测cpu利用率
    5. 3.5. 减少上下文切换
  4. 4. 并发程序的测试
© 2023 haoxp
Hexo theme