Java由volatile深入的一些问题

本文最后更新于:2022年12月19日 晚上

JMM(Java Memety Model) Java内存模型

首先了解一下JMM,JMM本身是一种抽象的概念,描述的是一组规范。

JMM关于同步的规范
  1. 线程解锁前,必须把共享变量的值刷新回主线程;
  2. 线程加锁前,必须把主内存的最新值读取到自己的工作内存;
  3. 加锁解锁是同一把锁。
JMM要求
  1. 可见性
  2. 原子性
  3. 有序性
有序性

有序性是指在单线程环境中, 程序是按序依次执行的。而在多线程环境中, 程序的执行可能因为指令重排而出现乱序。

知识点:

  • 内存可见性
  • 主存与线程之间内存的联系

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
2
3
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}

因为Java无法和操作系统底层进行交互,需要通过本地方法(native)来访问。Unsafe存在于sum.misc包中,其内部方法可以直接操作特定内存的数据,像C的指针一样直接操作内存。

注:Unsafe中所以方法都是native修饰的,也就是说Unsafe类中的方法都调用操作系统的底层资源执行相应任务。

变量valueOffset,表示该变量在内存中的偏移地址

CAS的功能判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

同时,因为比较并交换是系统原语,所以不会造成数据不一致的现象。

1
2
3
4
5
6
7
8
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;
}

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 协议 ,转载请注明出处!