Java由volatile深入的一些问题
本文最后更新于:2022年12月19日 晚上
JMM(Java Memety Model) Java内存模型
首先了解一下JMM,JMM本身是一种抽象的概念,描述的是一组规范。
JMM关于同步的规范
- 线程解锁前,必须把共享变量的值刷新回主线程;
- 线程加锁前,必须把主内存的最新值读取到自己的工作内存;
- 加锁解锁是同一把锁。
JMM要求
- 可见性
- 原子性
- 有序性
有序性
有序性是指在单线程环境中, 程序是按序依次执行的。而在多线程环境中, 程序的执行可能因为指令重排而出现乱序。
知识点:
- 内存可见性
- 主存与线程之间内存的联系
volatile关键字
volatile是java提供的轻量级的同步机制。
- 保证可见性
- 不保证原子性
- 禁止指令重排
原子性
原子性就是说一个操作不可以被CPU中途暂停然后调度,即不能被中断,要不就执行完, 要不就不执行。 如果一个操作是原子性的,那么在多线程环境下,就不会出现变量被修改等奇怪的问题。
禁止指令重排
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。保证多线程间的语义一致性。
实现方法:使用CPU内存屏障(Memery Barrier),在指令中插入一条Memery Barrier指令,会告诉编译器和CPU,禁止重排。
也就是说,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。
内存屏障的另一个作用就是强制刷新各种CPU的缓存数据,因此CPU上的任何线程都能读取到这些数据的最新版本。(可见性保证)
单例模式中,DCL双锁就使用了volatile防止多线程下指令被重排。
volatile的使用方式
(不做具体介绍)
为什么volatile不保证原子性?
CAS底层实现原理
关键词:unsafe
在volatile中,因为不保证原子性,所以在多线程下是不安全的,需要通过CAS实现原子性。
CAS:Compare and swap,比较并交换。
第一步,在线程从主存读取一个值 V=5 后,会暂存这个 V 为 A,称为旧的预期值,再将 A 进行修改,假设将 A + 1,这时候使用 B = A + 1进行修改,B称为更新值。
第二步将 B 这个值赋值给主存的V时,会先进行比较,如果主存中的这个V还==旧预期值A,那么说明这个V是没有被操作过的,那么就可以进行数值的更新 V=B;但是如果此时 V ≠ A,说明这个值已经被修改了,那么线程将会返回,重新执行 V+1的操作,直到这个过程成功。
这就是CAS的基本实现过程。
CAS源码实现
为什么CAS能实现原子性?靠的是底层的Unsafe类。
1 |
|
因为Java无法和操作系统底层进行交互,需要通过本地方法(native)来访问。Unsafe存在于sum.misc包中,其内部方法可以直接操作特定内存的数据,像C的指针一样直接操作内存。
注:Unsafe中所以方法都是native修饰的,也就是说Unsafe类中的方法都调用操作系统的底层资源执行相应任务。
变量valueOffset,表示该变量在内存中的偏移地址
CAS的功能判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
同时,因为比较并交换是系统原语,所以不会造成数据不一致的现象。
1 |
|
CAS的缺点
- 如果一直不成功,会给CPU带来很大开销;
- 只能保证一个变量的原子性操作;
- 会引发BAB问题。
ABA问题
此时:主存中值为A,线程1和线程2取出A,线程2将A改为B并写回主存,然后又将B改为A写回主存,主存中的值由A->B->A,改变了两次,之后才线程1修改了A,将值传回主存,发现值没变,成功写入。
尽管线程1操作成功,但这个过程也是存在问题的。
如何解决ABA问题?
带时间戳的原子引用
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!