1.并发编程的挑战

1.1 上下文切换

上下文切换也需要消耗时间和资源。

1.2 死锁

  • 加锁的方式不正确。A等待B持有的锁,B等待A持有的锁。且两者都不释放。

    1.3 资源限制

  • 比如带宽限制,3个人下载文件。一个人一个人的下和三个人同时下。在最终的结果上没有任何的区别

    1.4 小结

    本章是从一个宏观的角度去看待并发编程有可能出现的问题和注意点。一般解决的问题是:一个业务要不要使用并发编程? 当有这样问题出现时。我们要考虑并发编程这三个方面的挑战。

  1. 上下文切换是否值得
  2. 死锁现象要怎么避免
  3. 资源限制是否存在,开多线程是否有效果

    2.Java并发编程底层实现原理

    2.1 volatile实现原理

    volatile生成的字节码,通过JVM翻译成汇编码后,会多一个lock指令的生成。

    2.1.1 汇编级lock指令的作用

  4. 变量发生修改后,会立刻同步到主内存

  5. 同步到主内存后,会将其他缓存中的该变量状态置为失效

上面两条规则的实现方式,就是 缓存一致性协议。

2.1.2 使用优化

待补充

2.2 synchronized实现原理

synchronized关键字修饰的代码或方法,在字节码生成上有所不同

  • synchronized修饰方法:字节码对应的方法上有volatile的标记
  • synchronized修饰代码块:这段代码块的字节码前加入 monitorenter和monitorexit两个指令

    2.2.1 对象头的结构

    对象头=MarkWord+Klass Word+Array Length(数组对象独有)
    其中MarkWord主要标记着锁相关的一些信息

    2.2.2 MarkWord结构

    https://baijiahao.baidu.com/s?id=1722130078544093487&wfr=spider&for=pc
  1. 32位

image.png

  1. 64位

image.png

小疑问: 若我的无锁状态变化了,其他的锁状态如何记录hashCode的?

2.2.3 MarkWord中标记的四种锁

  1. 线程A运行到synchronized方法块位置
  2. 线程A检查MarkWord锁标志位为01
  3. 判断是否为偏向锁
  • 否:尝试CAS修改MarkWord的ThreadId。成功就可以执行同步方法了(over)失败的话需要跳入第4步
  • 是:
    • 若ThreadId记录的就是自己,则可以直接运行同步代码块(over)
    • 若不是自己,尝试CAS换成自己的Thread Id,成功就可以执行同步方法了(over)失败的话跳入第4步
  1. 开始偏向锁撤销工作
  2. 原持有偏向锁的线程到达安全点后暂停
  3. 检查原持有偏向锁的线程
  • 未活动/已退出同步代码块:原持有偏向锁的线程释放偏向锁,步骤回到第3步的分支
  • 未退出同步代码块:原持有偏向锁的线程升级为轻量级锁

image.png

2.2.5 轻量级锁加锁解锁过程

从偏向锁转换过来加锁过程

  1. 若在2.2.4的第6步中,发现原持有偏向锁的线程未执行完同步方法,锁升级为轻量级锁
  2. 原持有偏向锁的线程获取到轻量级锁,markword也被拷贝到自己的栈中
  3. 此时MarkWord锁状态为变为00,指向原持有偏向锁线程锁记录LockRecord的指针
  4. 原持有偏向锁线程被唤醒,从安全点继续执行。
  5. 当前线程,也就是线程B吧,CAS获取轻量级锁

    两个新进线程

    若时两个线程公平竞争一个轻量级锁的步骤如下:

  6. 将MarkWord复制到自己的线程栈中

  7. CAS将MarkWord中的指针修改为指向自己的Lock Record(在自己栈中)
    1. 成功就获取了锁
    2. 失败就自旋CAS修改MarkWord

解锁失败过程

正常的若线程A获取轻量级锁之中没有锁升级,正常释放锁就可以了。下面我们谈谈升级了锁的流程

  1. 线程A获取轻量级锁,线程B自旋获取锁,并没有获取到
  2. 线程B,修改MarkWord的锁状态为10,并自己进入阻塞队列(自闭) metux命令
  3. 线程A此时要释放锁,结果因为锁状态变为10了,所以失败。
  4. 此时线程A执行唤醒操作。

    2.2.6 重量级锁加锁过程

    没啥好说的,获取锁就执行,获取不到就进入阻塞队列等待。需要陷入内核态。
    获取到锁的线程:
    image.png

2.3 原子操作实现

CPU层面

  • 总线锁:阻塞其他CPU请求的方式
  • 缓存锁:

    • 所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,
    • 并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,
    • 处理器不在总线上声言LOCK#信号
    • 而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效
    • 总结,就是执行Lock指令时不锁总线,通过缓存一致性协议达到原子性

      Java层面

  • CAS

  • 3.Java内存模型

    3.1 Java内存模型基础

    Java内存模型基础主要对其相关概念进行一个总的阐述

    3.1.1 并发编程模型两个关键问题

  • 线程通信

  • 线程同步
  • 线程的通信时同步的基础,只有通信解决了,才能进行同步
    1. 线程通信的实现方式
  1. 共享内存:隐式
  2. 消息传递:显式

    这里作者认为Java的内存模型全称应该是Java基于共享内存的并发模型 既 JMM基于共享内存线程通信机制

3.1.2 JMM抽象结构

  • 工作内存
  • 主内存

    3.1.3 重排序

  1. 是什么?

为了提高程序执行的速度,编译器和处理器常常对指令进行重排序。这也是导致并发问题出现的关键因素。

  1. 重排序的种类
    1. 编译器优化重排序:不会改变单线程的语义,可以重排执行顺序
    2. 指令级并行的重排序:CPU对指令进行重排。
    3. 内存系统的重排序:主要是缓存的存在,导致读写上的顺序是乱序的
  2. 重排序会导致内存可见性问题。
    1. 操作的非原子性,导致需要多条指令
    2. 多条指令就会产生重排序
    3. 重排序就会导致可见性问题
  3. 解决方式
    1. 对于编译器重排序:JMM禁止特定类型编译器重排序
    2. 对于处理器级别重排序:采用插入特定类型内存屏障解决

3.1.4 并发编程模型分类

分类的依据是CPU对重排序的控制程度。具体的我这里就不说了

3.1.5 内存屏障

JMM解决CPU级别的指令重排序的手段就是插入内存屏障
那么,内存屏障的作用:防止指令重排序(废话)

  • StoreStore
  • StoreLoad:全能型
  • LoadLoad
  • LoadStore

3.1.6 happens-before规则

那么JMM给我程序员一个简单规则用于判断内存可见性问题。那就是happens-before规则

3.2 重排序

一切并发问题的根源

3.2.1 数据依赖性

操作B需要操作A的结果。

3.2.2 as-if-serial

不管怎么重排序,单线程运行的最终结果要和我没重排序时的结果一样。这就时仿佛串行的意思。

  1. 编译器不会对存在数据依赖的程序重排序

    3.2.3 程序顺序规则

    就是在单线程环境下,A,B。A代码写在前面,A就happens-beforeB。但是不代表A一定先比B执行。比如两个毫无关系的操作(没有数据依赖)A在B前面,满足happens-before,但是仍可以进行重排序。因为这样并不影响最终的结果

    3.2.4 重排序对多线程的影响

    影响巨大

    3.3 顺序一致性模型

    3.3.1 是什么(2大特性)

  2. 只看某一个线程的动作,一定和程序的代码顺序一致的

  3. 任何一个线程看到的整体顺序是一致的。每个操作原子执行并且对所有线程立即可见

image.png

3.3.2 JMM和其的关系

JMM的实现基于顺序一致性模型

3.4 同步原语

3.4.1 volatile

  1. 特性
    1. 可见性
    2. 单纯的读写操作具备原子性
  2. happens-before规则
    1. volatile的所有写操作happens-before后续的读操作
  3. 实现方式
    1. 写操作前插入LoadStore
    2. 写操作后插入StoreLoad
    3. 读操作后插入LoadLoad
    4. 读操作后插入LoadStore
  4. JSR-133改进

    1. 严格限制编译器和处理器对volatile变量与普通变量的重排序

      3.4.2 锁

  5. happens-before

    1. 锁的释放hapens-before锁的获取动作
    2. 先获取锁的同步代码块 happens-before 后获得锁的代码块
    3. 线程A释放锁之前对共享变量的修改对后面获得锁的线程可见、
  6. Synchronized内存语义
    1. 线程A释放锁实际是对接下来获取锁的线程发出了,改变了共享变量的消息
    2. 线程B获取锁接收了共享变量修改
  7. ReetrantLock内存语义

    1. 借助volatile的读写语义,因为用的是AQS框架 有个 volatile的共享变量

      3.4.3 final

  8. 特性

    1. 构造函数对final域的写入 先于 随后被这个对像被赋值给一个引用
    2. 初次读这个对象 先于 初次读对象里的final域
  9. 实现方式
    1. JMM禁止编译器将final域重排序到构造方法外
    2. 处理器级别,构造函数return前插入StoreStore屏障
  10. 外表特性

    1. final在构造函数内的赋值,是一定能不看到的。(不逃逸的情况下)
    2. final域是引用类型的,后面修改不保证可见性

      3.5 happens-before

      作为程序员是不希望自己通过自己对底层理解,来判断写的多线程程序是否正确的。
      因此,JMM提供了几个规则。就是happens-before
  11. 单线程下,操作A书写在操作B前,操作A happens-before 操作B(没数据依赖时,也可重排)

  12. 监视器锁的解锁 happens-before 加锁
  13. volatile的所有写操作 happens-before 后续的读操作
  14. Thread的start()方法的执行 happens-before,线程任务中的任意一个操作
  15. 线程B中的所有操作 happens-before 线程B的 join方法返回

    3.6 内存模型综述

    内存模型可以分为3大类,理想型,CPU级,语言级

参考文章

https://www.jianshu.com/p/36eedeb3f912