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/64bit | Mark Word | 存储对象的hashCode或锁信息等 |
| 32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
| 32/64bit | Array length | 数组的长度(如果当前对象是数组) |
java对象头里的Mark Word里默认存储对象的hashcode、分代年龄和锁标记位。32位jvm中存储结构如下:
| 锁状态 | 25bit | 4bit | 1bit是否偏向锁 | 2bit锁标志位 |
|---|---|---|---|---|
| 无锁状态 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
| 锁状态 | 23bit | 2bit | 4bit | 1bit是否偏向锁 | 2bit锁标志位 |
|---|---|---|---|---|---|
| 偏向锁 | 线程ID | Epoch | 对象分代年龄 | 1 | 01 |
| 锁状态 | 30bit | 2bit锁标志位 |
|---|---|---|
| 轻量级锁 | 指向线程栈中锁记录的指针 | 00 |
| 锁状态 | 30bit | 2bit锁标志位 |
|---|---|---|
| 重量级锁 | 指向互斥量的指针 | 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释放锁。