什么是Java内存模型
导致可见性的原因是缓存,导致有序性的原因是编译优化,那么解决可见性和有序性的最直接的方法就是禁用缓存和编译优化,但是如果禁用那么就会导致程序的性能下降。
所以合理的方案就是按需禁用缓存和编译优化,而何时禁用这件事情是程序员决定的,这时候就应该请出主角Java内存模型了。
Java内存模型是一个很复杂的概念,站在程序员的角度来看,Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括volatile,synchronized和final三个关键字以及六项Happens-Before规则。
使用volatile的困惑
volatile关键字并不是Java语言的特产,C语言也有,它最原始的意义就是禁用CPU缓存。
例如我们声明一个volatile变量
1 | volatile int x = 0; |
它表达的是:告诉编译器,对这个变量的读写,不能使用CPU缓存,必须从内存中读取或者写入。
例如以下代码,在1.5版本之前打印结果可能为0(因为CPU缓存的原因,线程2读取的是自身缓存的值),在JDK1.5之后肯定为40,因为在1.5版本之后JDK做了volatile的语意增强,这个增强其实就是一项Happens-Before规则。
1 | public class Test { |
Happens-Before规则
Happens-Before规则并不是先于发生的意思,而具体意思是指前一个操作的结果对于后一个操作时可见的。
程序的顺序性规则
在一个线程中,前面的操作Happens-Before后面的操作。
volatile变量规则
一个对于volatile变量的写操作Happens-Before于后续对这个变量的读操作。
传递性
如果 A Happens-Before B 且 B Happens-Before C 那么 A Happens-Before C
这条规则和volatile变量规则结合起来看前面的代码,其实就是上面代码示例的结果原理,在线程A中x=40的操作happens-before对于volatile变量v的写操作,然后线程A中对于volatile变量v的写操作happens-before于对线程B的对volatile变量v的读操作,所以x = 40happens-before线程B中的对于volatile变量v的读操作,也就是说x = 40对于线程B的那个操作是可见的,而JDK1.5之后就是通过这个原则来增强volatile语意的。
管程中的锁规则
管程是一种通用的同步原语。在Java中指的就是synchronized,synchronized就是java对管程的一种实现。
管程中的锁是java隐式帮我们实现的,在进入同步块代码的时候Java会帮我们上锁,执行完成会帮我们释放锁,这个是由编译器帮我们实现的,为的是防止程序员忘记释放锁。
1 | // 进入同步代码块加锁 |
线程的start()规则
主线程启动子线程B,子线程能看到主线程在启动子线程B之前的操作。换言之就是线程A中调用的线程B的start方法happens-before线程B中的任意操作。
1 | Thread B = new Thread(()->{ |
线程join规则
主线程等待子线程完成,当子线程完成后,主线程能看到子线程的操作。其实就是,在主线程A中调用了子线程B的join方法,这时候子线程B的任意操作happens-before主线程调用线程B的join方法
1 | Thread B = new Thread(()->{ |
线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
对象终结规则
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
final
final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化
总结
为什么定义Java内存模型?
现代计算机体系大部是采用的对称多处理器的体系架构。每个处理器均有独立的寄存器组和缓存,多个处理器可同时执行同一进程中的不同线程,这里称为处理器的乱序执行。在Java中,不同的线程可能访问同一个共享或共享变量。如果任由编译器或处理器对这些访问进行优化的话,很有可能出现无法想象的问题,这里称为编译器的重排序。除了处理器的乱序执行、编译器的重排序,还有内存系统的重排序。因此Java语言规范引入了Java内存模型,通过定义多项规则对编译器和处理器进行限制,主要是针对可见性和有序性。
三个基本原则
原子性、可见性、有序性。
Java内存模型涉及的几个关键词
锁、volatile字段、final修饰符与对象的安全发布。其中:第一是锁,锁操作是具备happens-before关系的,解锁操作happens-before之后对同一把锁的加锁操作。实际上,在解锁的时候,JVM需要强制刷新缓存,使得当前线程所修改的内存对其他线程可见。第二是volatile字段,volatile字段可以看成是一种不保证原子性的同步但保证可见性的特性,其性能往往是优于锁操作的。但是,频繁地访问 volatile字段也会出现因为不断地强制刷新缓存而影响程序的性能的问题。第三是final修饰符,final修饰的实例字段则是涉及到新建对象的发布问题。当一个对象包含final修饰的实例字段时,其他线程能够看到已经初始化的final实例字段,这是安全的。