【阅读】Java并发编程的艺术-Java并发机制的底层实现原理

80 阅读4分钟

volatile的应用

Java中Volatile关键字详解 www.cnblogs.com/zhengbin/p/…

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性:一个线程修改了一个共享变量后另一个线程能读取到修改后的值。

volatile比synchronized的使用和执行成本更低,不会引起线程上下文的切换和调度。

volatile的定义与实现原理

对由volatile修饰的变量进行写操作时,汇编指令中将多出一行lock前缀的指令。该指令会引发两件事情:

  • 将当前处理器缓存行的数据回写到系统内存。
  • 使其它CPU核心中对该内存地址的缓存无效。

volatile的使用优化

jdk7之前可以通过追加字节的方式,将对象填充至64字节,避免多个对象被写入同一个缓存行中,使它们不会相互锁定缓存行。

jdk7之后这个方法无效。

synchronized的实现原理与应用

jdk6对synchronized进行了优化,为了降低获得锁、释放锁带来的性能消耗,引入了偏向锁和轻量级锁。

java中每一个对象都可以作为锁,包括:

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步代码块,锁是synchronized括号里的对象。

jvm规范中,代码块同步使用monitorenter、monitorexit指令实现,其中:

  • monitorenter指令是在编译后插入到同步代码块的开始位置。
  • monitorexit指令是插入到代码块结束和异常的位置。
  • jvm保证每个monitorenter必须有对应的monitorexit,任何对象都有一个monitor与之关联,当且该monitor被持有后,对象处于锁定状态。
  • 线程执行到monitorenter时,将会尝试获取对象对应的monitor的所有权、即尝试获取对象锁。

Java对象头

synchronized使用的锁存储在java对象头中。对象头结构如下:

长度内容说明
32/64bitMark Word存储对象的hashCode或锁信息等
32/64bitClass Metadata Address存储到对象类型数据的指针
32/64bitArray length数组的长度(如果当前对象是数组)

java对象头里的Mark Word里默认存储对象的hashcode、分代年龄和锁标记位。32位jvm中存储结构如下:

锁状态25bit4bit1bit是否偏向锁2bit锁标志位
无锁状态对象的hashCode对象分代年龄001
锁状态23bit2bit4bit1bit是否偏向锁2bit锁标志位
偏向锁线程IDEpoch对象分代年龄101
锁状态30bit2bit锁标志位
轻量级锁指向线程栈中锁记录的指针00
锁状态30bit2bit锁标志位
重量级锁指向互斥量的指针10

锁的升级与对比

java se 1.6中,锁状态级别从低到高依次是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,以此提高获得锁和释放锁的效率。

偏向锁

大多数情况下锁总是由同一个线程多次获得,为了让线程获得锁的代价更低引入了偏向锁。

偏向锁的获得和撤销流程

sequenceDiagram
participant 线程1
participant 锁对象
participant 线程2

线程1->>线程1: 访问同步块
线程1->>锁对象: 检查对象头中是否存储了线程1的ID
锁对象->>线程1: 没有
线程1->>锁对象: CAS替换Mark Word:线程1的ID|Epoch|1|01
锁对象->>线程1: 成功
线程2->>线程2: 访问同步块
线程2->>锁对象: 检查对象头中是否存储了线程1的ID
锁对象->>线程2: 没有
线程2->>锁对象: CAS替换Mark Word:线程2的ID|Epoch|1|01
锁对象->>线程2: 失败
线程1->>线程1: 执行同步体
线程2->>线程1: 撤销偏向锁
线程1->>线程1: 全局安全点(safepoint)暂停线程
线程1->>锁对象: 解锁,将Mark Word设置为:空|0|01
线程1->>线程1: 恢复线程

关闭偏向锁

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

轻量级锁

轻量级锁及膨胀流程

sequenceDiagram
participant 线程1
participant 锁对象
participant 线程2
opt 无锁状态
线程1->>线程1: 访问同步块
锁对象->>线程1: 分配空间并复制MarkWord到线程栈
note right of 锁对象: 无锁:HashCode|age|0|01
线程2->>线程2: 访问同步块
锁对象->>线程2: 分配空间并复制MarkWord到线程栈
end
opt 轻量级锁
线程1->>锁对象: CAS替换Mark Word:指向线程栈锁对象
锁对象->>线程1: 成功
note right of 锁对象: 线程1锁对象指针|00
线程1->>线程1: 执行同步体
线程2->>锁对象: CAS替换Mark Word:指向线程栈锁对象
锁对象->>线程2: 失败
线程2->>锁对象: CAS自旋替换Mark Word
end
opt 升级为重量级锁
锁对象->>线程2: 失败
线程2->>锁对象: 锁膨胀为重量级锁
note right of 锁对象: 指向互斥量的指针|10
线程2->>线程2: 线程阻塞

线程1->>锁对象: 解锁,CAS替换Mark Word
锁对象->>线程1: 失败,因为MarkWord被修改
线程1->>锁对象: 释放锁并唤醒等待线程
note right of 锁对象: 0|10
线程2->>锁对象: 被唤醒
end

重量级锁

Java Synchronized 重量级锁原理深入剖析上(互斥篇) juejin.cn/post/700802…

原子操作的实现原理

原子操作是指不可被中断的一个或一系列操作。

处理器如何实现原子操作

  • 使用总线锁保证原子性
    • 当一个处理器在总线上输出LOCK #信号时,其它处理器的请求将被阻塞,那么该处理器就可以独占共享内存。
  • 使用缓存锁保证原子性
    • 处理器在某些场合使用缓存锁代替总线锁优化开销。

Java如何实现原子操作

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

使用循环CAS实现原子操作

jvm中的CAS操作使用处理器提供的CMPXCHG指令实现,自旋CAS的基本思路就是循环进行CAS操作直到成功为止。

CAS实现原子操作的三大问题

  • ABA问题:JDK提供了java.util.concurrent.atomic.AtomicStampedReference来解决ABA问题。
  • 循环时间长开销大:自旋CAS长时间执行会给CPU带来非常大的执行开销。
  • 只能保证一个共享变量的原子操作:无法保证对多个共享变量操作的原子性,可以通过java.util.concurrent.atomic.AtomicReference类来保证引用对象的原子性。

面试|详解CAS及其引发的三个问题 cloud.tencent.com/developer/a…
JUC中AtomicReference详解zhuanlan.zhihu.com/p/493964321

使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能操作锁定的内存区域,JVM内部除了偏向锁都用了循环CAS:当一个线程想进入同步块时使用循环CAS获取锁、退出同步块时使用循环CAS释放锁。