Java内存模型

image.png

  1. public class volatile_ {
  2. private static boolean initFlag=false;
  3. public static void main(String[] args) throws InterruptedException
  4. {
  5. new Thread(() -> {
  6. System.out.println("waiting data");
  7. while(!initFlag)
  8. {
  9. }
  10. System.out.println("========success");
  11. }).start();
  12. Thread.sleep(2000);
  13. new Thread(voliate::prepareData).start();
  14. }
  15. private static void prepareData()
  16. {
  17. System.out.println("prepare data begin");
  18. initFlag=true;
  19. System.out.println("prepare data end");
  20. }
  21. }

此代码运行结果为
image.png
所以 线程2虽然修改了initFlag的值 但是线程1并未跳出死循环 是因为线程2虽然修改了自己工作内存和主内存中的变量值,但是没有修改线程1工作内存中的变量值.所以线程1感知不到线程2改动的变量值.

解决办法: 给变量initFlag 加上volatile关键字

原子操作

  • lock(锁定)

  作用于主内存的变量,把一个变量标识为一条线程独占状态。

  • unlock(解锁)

  作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

  • read(读取)

  作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的Load动作使用

  • load(载入)

  作用于工作内存的变量,它把Read操作从主内存中得到的变量值放入工作内存的变量副本中

  • use(使用)

  作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎

  • assign(赋值)

  作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量

  • store(存储)

  作用于工作内存的变量,把工作内存中的一个变量的值传递到主内存中,以便随后的Write操作

  • write(写入)

  作用于主内存的变量,它把Store操作从工作内存中一个变量的值传送到主内存中的变量中。

image.png
程序没有加volatile关键字的运行情况

MESI缓存一致性协议

MESI(Modified Exclusive Shared Or Invalid)(也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出)是一种广泛使用的支持写回策略的缓存一致性协议,该协议被应用在Intel奔腾系列的CPU中,详见“support the more efficient write-back cache in addition to the write-through cache previously used by the Intel 486 processor”


MESI协议中的状态
CPU中每个缓存行(caceh line)使用4种状态进行标记(使用额外的两位(bit)表示):
M: 被修改(Modified)
该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。
当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。
E: 独享的(Exclusive)
该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。
同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。
S: 共享的(Shared)
该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,
其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。
I: 无效的(Invalid)
该缓存是无效的(可能有其它CPU修改了该缓存行)。

volatile可见性底层原理

volatile关键字修饰的变量会开启MESI协议 当initFlag变量被修改,通过总线store回主内存的时候,会被总线监听到,从而使其他cpu中的initFlag变量处于Invalid状态. 然后其他cpu会重新读取initFlag变量,这时候读取的就是已经被修改的变量了.
image.png
问题:线程1会不会在内存值还没有被修改的时候就嗅探到initFlag值被改变,从而重新载入错误的值
Answer:不会 0x01a3de24: ``**lock**`` addl $0x0,(%esp); 被volatile修饰的指令 进行写操作时 汇编指令会多一个lock前缀指令 。通过查 IA-32 架构软件开发者手册可知,lock 前缀的指令在多核处理器下会引发了两件事情。

  • 将当前处理器缓存行的数据会写回到系统内存。
  • 这个写回内存的操作会引起在其他 CPU 里缓存了该内存地址的数据无效。

Lock 前缀指令会引起处理器缓存回写到内存。Lock 前缀指令导致在执行指令期间,声言处理器的 LOCK# 信号。在多处理器环境中,LOCK# 信号确保在声言该信号期间,处理器可以独占使用任何共享内存。(因为它会锁住总线,导致其他 CPU 不能访问总线,不能访问总线就意味着不能访问系统内存),但是在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销比较大。在 8.1.4 章节有详细说明锁定操作对处理器缓存的影响,对于 Intel486 和 Pentium 处理器,在锁操作时,总是在总线上声言 LOCK#信号。但在 P6 和最近的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言 LOCK#信号。相反地,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据

也就是initFlag指令在store的时候会进行原子操作lock 写完之后才会unlock 所以其他cpu在没有解锁之前是不能重新载入内存的

一个处理器的缓存回写到内存会导致其他处理器的缓存无效。IA-32 处理器和 Intel 64 处理器使用 MESI(修改,独占,共享,无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32 和 Intel 64 处理器能嗅探其他处理器访问系统内存和它们的内部缓存。它们使用嗅探技术保证它的内部缓存,系统内存和其他处理器的缓存的数据在总线上保持一致。例如在 Pentium 和 P6 family 处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处理共享状态,那么正在嗅探的处理器将无效它的缓存行,在下次访问相同内存地址时,强制执行缓存行填充。

volatile原子性详解

volatile保证可见性和有序性,不保证原子性。

  1. public class Visio {
  2. public static volatile int num=0;
  3. public static void increase()
  4. {
  5. num++;
  6. }
  7. public static void main(String[] args) throws InterruptedException
  8. {
  9. Thread[] threads=new Thread[10];
  10. for(int i=0;i<threads.length;i++)
  11. {
  12. threads[i]=new Thread(new Runnable() {
  13. @Override
  14. public void run() {
  15. for(int i=0;i<1000;i++)
  16. {
  17. increase();
  18. }
  19. }
  20. });
  21. threads[i].start();
  22. }
  23. for(Thread t: threads)
  24. {
  25. t.join();
  26. }
  27. System.out.println(num);
  28. }
  29. }

此代码的运行结果不确定 值的范围为小于等于10000
image.png
原因: 线程1在store操作的时候 会给缓存加锁 这时候 如果线程2也完成assign操作 并要写回内存时 由于缓存已经被加锁 所以不能写回内存 又因为总线嗅探机制 会使线程2中的num值失效 所以线程2需要在线程1unlock之后重新read,load主内存中num值,相当于线程2有一次assign操作失效了 所以最后结果会小于等于10000

解决办法 给increase方法加上synchronized关键字