共计 1658 个字符,预计需要花费 5 分钟才能阅读完成。
请谈谈你对 volatile 的理解?
volatile 是 java 虚拟机提供的轻量级的同步机制
三大特征:(1)保证可见性
当线程对变量进行操作时,必须在工作内存中进行,完成后再写会到主内存中。在写回主内存中后其他线程马上都能看到,就是可见性(2)不保证原子性
丢失写值的情况(3)禁止指令重拍
JMM 内存模型
可见性
原子性
有序性
主内存:主内存是共享内存的区域,相当于我们的内存条,里面存储共享变量,所有线程都能访问
工作内存:每个线程创建时 JVM 都会为其创建一个工作内存,工作内存里面存放的是该线程的私有数据,不能相互访问,当线程对变量进行操作时,必须在工作内存中进行,完成后再写会到主内存中。在写回主内存中后其他线程马上都能看到,就是可见性
什么是 CAS(compareAndSet)?
比较并交换
AtomicInteger.compareAndSet(期望值,要修改的值)
期望值:从主物理内存中的值
线程每次修改值的时候都要从主物理内存中去获取并比较是否和期望值相同,如果和期望值相同,则修改成要修改的值,并返回 true
CAS 底层原理?
unsafe valueoffset(内存偏移地址)
Unsafe 是 CAS 的核心类
atomicInteger.getAndIncrement();
底层源码:::::
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
假设 线程 A 和 线程 B 同时执行 getAndAddInt 操作,
首先线程 A 先从主内存中获取到变量 A = 3 到自己的线程内存中,
同时线程 B 也从主内存中获取到变量 A = 3 到自己的线程内存中,
现在 AB 线程中都有一份自己的内存变量 A = 3 的副本,
线程 A 通过 this.getIntVolatile(var1, var2);
获取到内存中的 A =3,这时线程 A 被挂起。
这时。线程 B 通过 this.getIntVolatile(var1, var2);
获取到内存中的 A =3,并执行了 this.compareAndSwapInt(var1, var2, var5, var5 + var4)
比较主内存中的值也为 3,就成功将自己的工作内存中的 A 修改为 4 并写回到主内存中。
这时线程 A 回复,执行 compareAndSwapIn 方法比较,发现自己工作内存中的 A 与主内存中的 A
不一致,说明已经被其他线程修改过了,那线程 A 本次修改失败,while 重新来一遍
线程 A 重新获取 value,因为变量被 volatile 修饰,所以其他线程对他的修改线程 A 总是能看到
(volatile 的可见性原理)线程 A 继续执行 compareAndSwapInt 进行比较直到成功
缺点:
循环时间开销很大
只能保证一个共享变量的原子操作
ABA 问题(狸猫换太子)
CAS 算法是提取内存中某时刻的数据并在当下时刻进行比较并替换,这个时间差会导致数据变化
比如 T1 从内存位置 V 中取出 A,这时另一个线程 T2 也取出 A,T2 将 A –>B,然后 T2 又将 B –>A,这
时 T1 进行 CAS 发现内存中还是 A,然后线程 T1 就操作成功了
解决 ABA 问题:
原子引用 AtomicReference<T>
新增一种机制,修改版本号 AtomicStampedReference(初始值,初始版本号)
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
期望值,要修改的值,期望版本号,要修改的版本号