使用并发带来的挑战和解决方案
上下文切换
切换定义:某个线程从上一次(使用CPU时间片)保存状态退出执行到下一次加载执行
影响:切换带来开销
评估:Lmbench3 测量切换时长,vmstat 测量切换次数
解决:无锁并发(如取模分片),CAS算法,避免创建不必要线程,单线程多任务(协程)
实战:减少web容器配置的maxThreads (根据实际情况确定)
死锁
原因:互相等待对方释放锁
场景:异常导致没有到释放环节,或者释放本身异常
解决:避免一个线程同时获取多个锁、占用多个资源,尝试定时锁
硬软件资源限制
示例:带宽、硬盘读写、CPU速度,数据库、socket连接数
问题:并行退化为串行,额外增加了切换开销
解决:集群增加资源,池复用资源,确定并发数时考虑资源限制/瓶颈
* 强烈建议多使用JDK并发包提供的并发容器和工具类 *
底层实现原理
JVM&CPU指令
volatile
如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的
volotile变量写操作会多一个lock指令:将当前处理器缓存数据写回系统内存,一个处理器的缓存回写到内存会导致其他处理器的缓存无效
处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。
使用优化:jdk7引入的LinkedTransferQueue使用追加字节方式,使得队列头尾节点大小刚好符合处理器缓存一个缓存行大小(如64字节),使得头尾节点位于不同缓存行,修改时不互相影响(锁定缓存行),从而加快入队、出队并发速率
synchronized
3种形式
- 普通同步方法,锁当前实例对象
- 静态同步方法,锁对应class对象
- 同步方法块,括号中配置对象
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁
不同锁状态
- 无锁
- 偏向锁
- 场景:大部分情况下锁总是被同一线程多次获得
- 解决:对象头MarkWord存储对应锁线程id,以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁
- 释放锁:其它线程竞争该锁时才释放,偏向锁释放需要等待全局安全点
- 轻量级锁
- 使用自旋来竞争
- 使用自旋来竞争
- 重量级锁