Volatile

JMM

Java 内存模型,是虚拟机规范中所定义的一种内存模型,Java 内存模型是标准化的,屏蔽掉了底层计算机之间的区别。
  Java 内存模型描述了 Java 程序中各种变量(线程共享变量)的访问规则,以及在 JVM 中将变量,存储到内存和从内存中读取变量这样的底层细节。

规定

所有的共享变量都存储于主内存,这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,不存在竞争问题。
   线程对所有变量的操作都必须在工作内存中完成,而不能直接操作主内存中的变量。
   不同的线程也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。


可见性问题

通过加 synchronized 来解决可见性问题:

因为某一个线程进入 synchronized 代码块前后,线程会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存成为副本,执行代码,将修改后的副本的值刷新回主内存中,线程释放锁。
   而获取不到锁的线程会阻塞等待,所以变量的值肯定一直都是最新的。

通过 volatile 解决可见性问题:

每个线程操作数据的时候会把数据从主内存读取到自己的工作内存,如果他操作了数据并且写回了,其他已经读取的线程的变量副本就会失效了,需要对数据进行操作又要再次去主内存中读取了。


缓存处理

缓存一致性协议

多个处理器的运算任务涉及同一块主内存区域时,将导致各自的缓存数据不一致,问题在于同步回主内存时以谁的缓存数据为准。
   为了解决一致性的问题,需要各个处理器在访问缓存时都遵循一些协议,在读写时要根据协议操作,这类协议有 MSI,MESI,Synapse,Firefly,Firefly 等。

MESI

当 cpu 写数据时,如果发现操作的变量是,即在其他 cpu 中也存在该变量的副本,会发出信号通知其他 cpu 将该变量的缓存行设置为无效状态,因此当其他 cpu 要读取这个变量时,会发现自己缓存中缓存该变量的缓存行是无效的,那么他就会从内存重新读取。

嗅探:

每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作时,会重新从系统内存中把数据读到处理器缓存里。

总线风暴:

由于 volatile 的 MESI 缓存一致性协议,需要不断的从主内存嗅探和 CAS 不断循环,无效交互会导致总线带宽达到峰值。所以不应该大量使用 volatile,什么时候使用 volatile 什么时候使用锁,需要具体判断。


指令重排序

为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。

过程:

源代码——>编译器优化重排序——>指令级并行重排序——>内存系统重排序——〉最终执行指令序列

一个好的内存模型,实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标,而进行奋斗。在不改变程序执行结果的前提下,尽可能提高运行效率。
  JMM 对底层减少约束,使其能够发挥自身优势。

三种重排序

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来讲多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得读取和存储操作看上去可能是在乱序执行的。

as-if-serial:

不管怎么重排序,单线程下的执行结果不能被改变。
   编译器、runtime 和处理器都必须遵守 as-if-serial 语义。

内存屏障:

java 编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。
   为了实现 volatile 的内存语义,JMM 会限制特定类型的编译器和处理器重排序,JMM 会针对编译器制定 volatile 重排序规则表:

是否能重排序第二个操作普通读写vol 读vol 写

第一个操作
普通读写||||NO
vol 读||NO|NO|NO
vol 写|||NO|NO

需要注意的是:

  1. volatile 写是分别在前面和后面插入内存屏障.
  2. 而 volatile 读是在后面插入两个内存屏障。

写内存屏障:

普通读——>普通写——>storestore 屏障(禁止上面写和其他重排序)——>vol 写——>storestore 屏障(禁止下面读和其他重排序)

读内存屏障:

vol 读——>loadload 屏障(禁止下面读重排序)——>loadload 屏障(禁止下面写重排序)——>普通读——>普通写

为了提高处理速度,JVM 会对代码进行编译优化,也就是指令重排序优化,并发编程下指令重排序会带来一些安全隐患:如指令重排序导致的多个线程操作之间的不可见性。
   从 JDK5 开始,提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。

happens-before:

happens-before
   如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。
  volatile 域规则:对一个 volatile 的写操作,happens-before 于任意线程后续对这 volatile 域的读。

volatile 无法保证原子性:

假设现在有 N 个线程对同一个变量进累加也是没办法保证结果是对的,因为读写这个过并不是原子性的。
   要解决也简单,要么用原子类,比 AtomicInteger,要么加锁(记得关注 Atomic 的底层。

volatile 与 synchronized 的区别:

volatile 只能修饰实例变量和类量,而 synchronized 可以修饰方法,以及代码块。
  volatile 保证数据的可见性,但是保证原子性(多线程进行写操作,不保证线程安全)而 synchronized 是一种排他(互斥)的机制。    volatile 用于禁止指令重排序:可以解决例双重检查对象初始化代码执行乱序问题。
  volatile 可以看做是轻量版 synchronized,volatile 不保证原子性,但是如是对一个共享变量进行多个线程的赋值,     而有其他的操作,那么就可以用 volatile 来代 synchronized,因为赋值本身是有原子性的,volatile 又保证了可见性,所以就可以保证线程安了。


总结

  1. volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如 boolean flag;或者作为触发器,实现轻量级同步。
  2. volatile 属性的读写操作都是无锁的,它不能替代 synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的。
  3. volatile 只能作用于属性,我们用 volatile 修饰属性,这样 compilers 就不会对这个属性做指令重排序。
  4. volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile 属性不会被线程缓存,始终从主存中读取。
  5. volatile 提供了 happens-before 保证,对 volatile 变量 v 的写入 happens-before 所有其他线程后续对 v 的读操作。
  6. volatile 可以使得 long 和 double 的赋值是原子的。
  7. volatile 可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。