原子性,可见性,有序性

  • 原子性:原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
  • 可见性:可见性指的是在多线程环境下,一个线程修改了某个变量的值,其他线程是否能够马上感知到这个变量的新值。
  • 有序性:在JVM的即时编译器和计算机处理器中,在把程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致。处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。所以这将导致在多线程情况下,如果一个线程依赖另一个线程的计算结果,在代码上看似没有问题,实际却会有意想不到的的情况发生,具体在下篇文章valotile例子中演示。

JMM如何解决原子性&可见性&有序性问题

  • 原子性:对于基本数据类型的读写,在JVM中本身就是原子性的,其他的情况可以使用synchronizedLock实现原子性,因为synchronizedLock保证了任意时刻只有一个线程能访问该代码块。
  • 可见性:可通过volatile关键字进行修饰,被volatile修饰的共享变量,保证了每次修改必须立即更新到主内存,其他线程读取时,也必须去内存读取新值。synchronizedLock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中(线程上下文切换)。
  • 有序性:使用volatile关键字可保证一定的有序性,也可通过synchronizedLock保证。

Happen-Before 原则

Happens-Before的八个规则(摘自《深入理解Java虚拟机》12.3.6章节):

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 管程锁定规则:一个unLock操作先行发生于后面对同一个锁lock操作;(此处后面指时间的先后)
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作,保证任何时刻不同的线程总是能读取到共享变量的最新值;(此处后面指时间的先后)
  4. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;
  5. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
  8. 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

As-If-Serial语义

**指令重排序: **java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。

指令重排序的意义:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

As-If-Serial语义: 不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

/*以下例子当中1、2步存在指令重排行为,但是1、2不能与第三步指令重排
也就是第3步不可能先于1、2步执行,否则将改变程序的执行结果*/
double p = 3.14; //1
double r = 1.0; //2
double area = p * r * r; //3计算面积