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

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的方式来获取和释放锁

posted @ 2021-03-11 20:16  菜鸟kenshine  阅读(217)  评论(0编辑  收藏  举报