• 并发编程(二):并发机制的实现


    1.依赖JVM

    java代码编译为字节码,JVM执行字节码生成汇编指令,CPU执行汇编指令。

    java并发机制依赖于JVM的实现和CPU指令。

    2.Volatile实现原理

    轻量级Sycronized,保证了共享变量的可见性

    保证一个线程修改一个共享变量后,另一个线程总是能够读到这个修改后的变量值

    2.1 相关CPU术语

    • 内存屏障:一组处理器指令,用于限制对内存操作的顺序
    • 缓冲行:cache line,缓存线,缓存中可以分配的最小存储单位
    • 原子操作:不可中断的一个或一系列操作
    • 缓冲行填充:处理器识别到从内存读取的操作数是可缓存的时,就读取整个缓存行填充到适当的缓存
    • 缓存命中:缓存行填充的位置是下一次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存中
    • 写命中:处理器将操作数回写时,会检查这个缓存的内存地址是否在缓存行中,存在则写回缓存而不是主存,称为命中
    • 写缺失:一个有效缓存行被写入到不存在的内存区域

    2.2 实现原理

    处理器缓存(cpu cache line),系统内存,处理器操作的是工作内存的数据。

    对声明volatile的变量进行写操作,JVM会向处理器发送一条lock前缀指令(lock addl),lock前缀指令在多核处理器下会引发两件事情:

    • 将当前处理器缓存行的数据写回到系统内存
    • 这个写回内存的操作会使其他cpu中缓存了该地址内存的缓存行失效

    缓存一致性协议:保证各个处理器的缓存是一致的

    2.3 两条实现原则

    缓存一致性机制:阻止同时修改两个以上处理器缓存的内存区域数据。

    • LOCK指令前缀在执行期间会声言处理器的LOCK#信号,确保处理器能独占任何共享内存
    • 有些处理器(Intel486等)是直接在总线上声言LOCK#信号的,锁总线,开销大——总线锁定
    • 现代处理器,如果访问的内存区域的缓存在处理器内部,则会锁缓存行——缓存锁定

    缓存失效:一个处理器缓存回写到系统内存会导致其他处理器缓存失效。

    • MESI控制协议:维护内部缓存和其他处理器缓存的一致性
      • Modified:修改
      • Exclusive:独享
      • Shared:共享
      • Invalid:无效
    • 许多处理器使用嗅探技术保证它的内部缓存,系统内存和其他处理器缓存的数据在总线上保持一致
    • 嗅探一个处理器,其他处理器打算写内存地址,嗅探的处理器会使内存地址对应的缓存行失效,下一次访问相同地址(写的时候),会强制执行缓存行填充

    2.4 Volatile使用优化

    追加到64字节——LinkedTransferQueue,使用追加字节的方式来优化队列出队入队的性能

    很多处理器缓存行为64字节宽,且不支持部分填充,将共享变量追加到64字节大小,可以避免队列头尾节点加载到同一缓存行中,使头尾节点不会互相锁定,可以单独进行操作。

    两种情景下不该使用这种方式:

    • 缓存行非64字节宽的处理器
    • 共享变量不会被频繁地写的时候

    这种追加方式在Java7中可能不生效,会淘汰和重新排列无用字段,需要使用其他追加字节的方式

    3.Sycronized实现原理

    Sycronized操作都很重量级,但是1.6版本对Sycronized进行了各种优化:

    • 引入偏向锁和轻量级锁,减少获得锁和释放锁的性能消耗
    • 锁的存储结构升级

    三种形式的Sycronized锁:

    • 普通同步方法,锁当前实例对象
    • 静态同步方法,锁类的class对象
    • 同步代码块,锁Sycronized()中配置的对象

    3.1 实现原理

    JVM基于进入和退出Monitor对象来实现方法同步和代码块的同步

    • 代码块同步是通过指令monitorenter和monitorexit实现的
    • 方法同步则是另一种方式实现的,但可以使用这两个指令实现

    monitorenter指令插入到同步代码块开始位置,monitorexit指令插入到结束位置;
    JVM要保证以下两点:

    • 每个monitorenter指令必须有对应的monitorexit指令对应
    • 任何对象都有一个moniter与之关联。一个monitor被持有则会进入锁定状态。

    执行到monitorenter指令会尝试获取对象所对应的monitor所有权

    3.2 Java对象头

    Synchronized的锁是存在Java对象头中的

    如果对象是数组,用3个字来存储对象头,非数组对象用两个字存储对象头。

    • 32位虚拟机:1字=4字节=32位(bit)
    • 64位虚拟机:1字=8字节=64位(bit)

    Java对象头长度:(2或3个字)

    • Mark Word:标记字,Class Metadata Address:类元数据地址
    • 数组会包含第三个字:Array length,非数组没有
    • Array length数组长固定为32位,另外两个视JVM位数而定
    • 锁数据存放在Mark Word中

    32位JVM中Mark Word默认存储结构:(25412)

    不同锁在32位JVM的Mark Word中的存放形式:

    64位JVM中Mark Word默认存储结构:

    3.3 锁升级优化(jdk1.6)

    四种锁,级别从低到高:无锁,偏向锁,轻量级锁,重量级锁

    锁级别可以升但是不能降,级别越高越重。

    偏向锁

    CAS操作:Compare and Swap 比较并交换,常用来加锁和解锁。

    锁对象的对象头Mark Word中有一位(bit)用来表示该对象锁是否是偏向锁(偏向锁标识)。

    大多数情况下,锁不仅不存在竞争而且总是由同一线程多次获得,其中CAS的加解锁操作就花费了很大的代价。偏向锁可以通过相关数据结构减少CAS操作数量,提高应用性能。

    偏向锁只有等竞争出现才释放锁。当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁

    偏向锁获取流程图:

    偏向锁撤销流程图:

    JVM相关参数:

    • 关闭偏向锁启动延迟:-XX:BiasedLockingStartupDelay=0

    • 关闭偏向锁:-XX:-UseBiasedLocking

    轻量级锁(自旋锁)

    存在线程竞争,但是竞争的线程不阻塞。升级为重量级锁后会阻塞,且不能降级。

    • 轻量级锁加锁:(会将MarkWord复制一份再修改)

    • 轻量级锁解锁:

    不同锁的比较

    • 偏向锁:适用于只有一个线程访问同步块的场景

      加锁解锁消耗非常小,多个线程竞争会产生撤销偏向锁的额外开销

    • 轻量级锁:追求响应时间,同步块执行速度非常快时使用

      若同步块执行慢,自旋会一直消耗cpu

    • 重量级锁:追求吞吐量,同步快执行时间长时使用

      线程阻塞,响应时间慢

    4.原子操作实现原理

    原子操作,不可被分割的一个或一系列操作

    4.1 相关术语

    • 缓存行:缓存最小操作单位

    • CAS:比较并交换,需要输入两个数,一个旧值,一个新值,旧值没有发生变化才会进行交换成新值

    • cpu流水线:指令处理流水线,将指令分解为5-6步后由5-6个不同的功能单元分别执行

    • 内存顺序冲突:多个cpu同时修改一个缓存行的不同部分引起其中一个cpu操作无效

      出现内存顺序冲突必须清空流水线

    4.2 实现原子操作

    处理器会保证简单内存操作的原子性,总线锁缓存锁保证复杂内存操作的原子性

    总线锁定

    处理器提供一个LOCK#信号,一个处理器在总线上输出此信号,其他处理器请求将被阻塞,该处理器独占共享内存。总线锁开销大,并且会导致其他处理器无法处理其他内存地址的数据。

    缓存锁定

    内存区域如果被缓存在缓存行中,且在锁操作期间被锁定,那么锁操作回写到内存时不会再总线上输出Lock#信号,而是修改内部的内存地址。其他处理器回写已被锁定的缓存行的数据时,会使缓存行失效。

    两种情况下处理器不会使用缓存锁定

    1. 操作数不能被缓存,或者操作数跨多个缓存行,会使用总线锁定
    2. 有些处理器不支持缓存锁定,就算锁了缓存,也会调用总线锁

    这两种情况可以通过Intel处理器提供的Lock操作前缀指令来解决,被这些指令操作的内存区域就会加锁:

    • 位测试与修改:BTS,BTR,BTC
    • 交换:XADD,CMPXCHG
    • 操作数和逻辑指令:ADD,OR

    4.3 Java实现原子操作

    Java中可以通过锁和循环CAS操作实现原子操作

    循环CAS操作实现

    • JVM中的CAS操作基于处理器的CMPXCHG指令实现

    • Java从1.5开始提供了一些原子类:AtomicBoolean,AtomicInteger等等,其中提供了CompareAndSet方法实现CAS操作

    • 自旋CAS:循环CAS操作直到成功为止

    循环CAS操作的三个问题

    • ABA问题:原值从A变B再变A会判定为没变化

      AtomicStampedReference(jdk1.5)类的CompareAndSet方法可以解决这个问题

    • 循环时间长开销大:CAS长时间不成功会浪费大量cpu资源

    • 只能保证一个共享变量原子操作:多个共享变量可以使用锁,或者将多个共享变量合并为一个

      jdk1.5提供了AtomicReference可以确保引用对象之间的原子性(可以将多个变量放入引用对象)

    锁机制实现

    JVM内部实现了多种锁:偏向锁,轻量级锁(自旋锁),互斥锁

    除了偏向锁,其他锁都通过自旋CAS的方式来获取和释放锁

  • 相关阅读:
    POJ 2752 KMP中next数组的理解
    KMP详解
    HDU 3221 矩阵快速幂+欧拉函数+降幂公式降幂
    POJ 3220 位运算+搜索
    反素数深度分析
    POJ 2886 线段树单点更新
    求反素数的方法
    CV第八课 GPU/CPU
    49. 字母异位词分组
    48. 旋转图像
  • 原文地址:https://www.cnblogs.com/kenshine/p/14520263.html
Copyright © 2020-2023  润新知