0%

《java并发编程的艺术》笔记

使用并发带来的挑战和解决方案

上下文切换

切换定义:某个线程从上一次(使用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操作来加锁和解锁
    • 释放锁:其它线程竞争该锁时才释放,偏向锁释放需要等待全局安全点
  • 轻量级锁
    • 使用自旋来竞争
  • 重量级锁