Java内存模型JMM与可见性问题

在讲述Java内存模型之前,先介绍几个概念:指令重排序,Java运行模式

1.指令重排序

java编程语言的语义允许编译器和处理器执行指令重排序优化,以提高程序运行效率。指令重排序需遵守as-if-serial,as-if-serial指的是:不管怎么重排序,单线程程序的执行结果不能被改变。编译器和处理器都必须遵守as-if-serial语义。也就是说,编译器和处理器不会对存在数据依赖关系的操作做重排序。

指令重排序分为:重排序和等效替换

 

2.java运行模式

  • 编译:字节码----jit提前编译----汇编
  • 解释:字节码----jit一段一段编译----汇编
  • 混合:综合上述两种,运行的过程中,jit编译器生效,针对热点代码进行重排序优化并预编译。可使用jitwatch查看优化后的汇编代码

 

java内存模型

java内存模型描述程序可能的行为,通过检查执行跟踪的每个读操作,并根据某些规则检查该读操作观察到的写操作是否有效来工作。程序执行产生的所有结果都可以由内存模型预测。

具体来说,Java内存模型主要目标是定义程序中各个变量的访问规则。所有的变量都是存在主存中,每个线程都有自己的工作内存。线程对变量的操作都必须在工作内存进行,不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。 

Java内存模型对于多线程三大特性的规定:

  • 原子性:java内存模型规定基本数据类型的访问读写具备原子性
  • 可见性:通过一套同步规则,将变量修改后的新值同步回主内存,在变量读取前从主内存刷新变量值到工作内存,实现可见性
  • 有序性:本线程内观察到的所有操作都是有序的,但是在另一个线程中观察该线程,由于重排序和同步延时,所有操作都是无序的。

Java中可见性存在CPU缓存等导致短期内可见性问题,重排序可能导致永久可见性问题。

可见性同步规则:

  1. 对于监视器m的解锁,与所有后续操作对于m的加锁同步
  2. 对volatile变量v的写入,与所有后续对v的读同步
  3. 启动线程的操作,与线程中的第一个操作同步
  4. 对于每个属性写入默认值(0、false、null),与每个线程对其进行的操作同步
  5. 线程t1的最后操作,与线程t2发现t1已经结束同步(isAlive、join)。
  6. 如果线程t1中断了t2,那么线程t1的中断操作与所有线程发现t2被中断同步(InterruptedException,interrupted,isInterrupted)

Happens-before先行发生原则

happens-before关系主要用于强调两个有冲突的动作之间的顺序。对于共享变量的多次访问,如果至少有一次是写,那么对于该变量访问是冲突的。当程序包含两个没有被happens-before关系排序的冲突访问时,就存在数据竞争。遵守这些原则,也就意味着有些代码不能进行重排序,有些数据不能缓存。

虚拟机实现时,必须要确保以下原则(happens-before可以理解为先发生对后发生可见)

  1. 某个线程中的每个动作都happens-before该线程中该动作之后的操作
  2. 某个monitor上的unlock动作happens-before同一monitor上的后续的lock操作
  3. 对某个volatile字段的写操作happens-before每个后续对该volatile字段的读操作
  4. 在某个线程对象上调用start()方法happens-before该启动了的线程中的任意动作
  5. 某个线程中的所有动作happens-before任意其他线程成功从该线程对象上的join()中返回
  6. 具有传递性,如果某个动作a happens-before b,且b happens-before c,那么a happens-before c

volatile关键字

volatile关键字可以让一个线程对共享变量的修改,能够及时被其他线程看到,遵循Java内存模型同步规则。

  • 禁止缓存:volatile变量的访问控制符会加ACC_VOLATILE,保证立即可见
  • 禁止重排序:对volatile变量相关的指令不做重排序

volatile不保证原子性

final在JMM中的处理

  • 在共享对象的构造函数中设置的final字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版本。示例:f=new FinalDemo();若x为final字段,则读取到的f.x一定是最新的
  • 在共享对象构造函数中设置字段后发生读取,如果该字段为final,会看到字段分配的值,否则会看到默认值。示例:public FinalDemo(){x=1;y=x},y会等于1
  • 读取共享对象的final字段时,要先读取共享对象。示例:f= new FinalDemo();a=f.x;这两个操作不能重排序
  • 通常final static是不可修改的字段,然而System.in、System.out、System.err是final static字段,由于遗留原因,必须允许通过set方法改变,我们见这些字段称为写保护,以区别于普通final字段

double和long的特殊处理

虚拟机规范中,写64位的double和long分成了两次32位的操作,由于不是原子操作,可能导致读取到的某次写操作中的前32位,以及另一次写操作的后32位。读写volatile的double和long总是原子的,引用的读写也总是原子的

虽然规范没要求实现原子性,但是商业JVM大部分实现了原子性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章