一起学并发《3》java内存模型

198 阅读3分钟


在我们学习并发编程的时候,也许会经常碰到这中问题,假设一个线程为变量aVariable赋值,我们怎么才能读取到这个变量呢?

aVariable = 3;

似乎这个问题听起来很愚蠢,但是如果缺少同步,那么将会有许多因素导致线程无法立即甚至永远,看到另一个线程的操作结果。编译器会将源码进行重排序,此外编译器也许将变量存储到寄存器里而不是存入内存;处理器可以采用乱序或者并行的方式来执行指令;缓存可能会改变将写入变量提交到主内存的顺序;而且,保存在处理器本地缓存的值,对于其他处理器是不可见的。这些因素都有可能导致,一个线程无法看到这个变量的最新值!

那么在java中我们又是怎么确保线程之间的可见性的呢?当然,出现问题了,肯定有解决问题的办法,下面让我们一起来学习我们聪明的前辈是怎么解决的吧!

1.happens-before规则

程序顺序规则:如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行

监视器锁规则:在监视器上的解锁操作一定在同一个监视器锁上的加锁操作之前执行。

volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前执行。

线程启动规则:在线程上对Thread.start的调用必须在该线程中执行任何操作之前执行。

传递性:如果操作A在操作B之前,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

2.重排序

2.1 源代码到指令序列的重排序

在执行程序是,为了提高性能,编译器和处理器常常会对指令做重排序,重排序一共有三种类型。

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

2.2 数据依赖性

如果有两个操作都访问了同一个变量,并且其中一个操作是对这个数据的写操作,那么这两个数据就存在数据依赖性

编译器和处理器对指令的重排序,都遵循数据依赖性的原则,优化指令执行效率的同时会考虑到线程执行的结果。

2.3 as-if-serial语义

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程的执行结果不能被改变。编译器、runtime和处理器都必须遵循as-if-serial语义。为了说明,请看下面的代码示例:

  double a = 3.14; //A
  double b = 5.0; //B
  double area = a*a*b;//C

A和C存在依赖关系,B和C存在依赖关系,那么A和B执行的顺序无论怎么变化,都不会影响C的结果。