JMM 系列:
Java 内存模型就是为了解决多线程环境下共享变量的一致性问题,那么一致性包含哪些内容呢?
一致性主要包含三大特性:原子性、可见性、有序性,下面我们就来看看Java内存模型是怎么实现这三大特性的。
1.原子性
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
X=10; // 原子性(简单的读取、将数字赋值给变量)
Y = x; // 变量之间的相互赋值,不是原子操作
X++; // 对变量进行计算操作,不是原子操作
在java中,对基本数据类型的变量的读取和赋值操作是原子性操作有点要注意的是,对于 32位系统的来说,对于基本数据类型,byte,short,int,float,boolean,char读写是原子操作;而long类型数据和double类型数据,它们的读写并非原子性的,因为对 于32位虚拟机来说,每次原子读写是32位的,而long和double则是64位的存储单元。
也就是说如果存在两条线程同时对long类型或者double类型的数据进行读写,是存在相互干扰的。这样会导致一个线程在写时,操作完前32位的原子操作后,轮到B线程读取时,恰好只读取到了后32 位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它可能是“半个变量”的数值,即64位数据被两个线程分成了两次读取。
但也不必太担心,因为读取到“半个变量”的情况比较少见,至少在目前的商用的虚拟机中,几乎都把64位的数据的读写操作作为原 子操作来执行,因此对于这个问题不必太在意,知道这么回事即可。
JMM 解决方案
1)我们可以大致认为基本类型变量的读写是具备原子性的。
2)如果应用需要一个更大范围的原子性,Java内存模型还提供了lock和unlock这两个操作来满足这种需求,尽管不能直接使用这两个操作,但我们可以使用它们更具体的实现synchronized来实现。因此,synchronized块之间的操作也是原子性的。
2.可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。
PS:对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。
但在多线程环境中可就不一定了,前面的文章我们分析过,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见。这种工作内存与主内存同步延迟现象就造成了可见性问题。
另外,指令重排以及编译器优化也可能导致可见性问题;无论是编译器优化还是处理器优化的重排现象,在多线程环境下,确实会导致程序轮序执行的问题,从而也就导致可见性问题。
JMM 解决方案
1)Java提供了volatile,当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。
2)synchronized和Lock也可以保证可见性,synchronized的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store和write操作”这条规则获取的。
3.有序性
Java程序中天然的有序性可以总结为一句话:如果在本线程中观察,所有的操作都是有序的;如果在另一个线程中观察,所有的操作都是无序的。
前半句是指线程内表现为串行的语义,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。
因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致。即只要程序的终结果与 它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处理器等) 适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,大限度的发挥机器性能。 下图为从源码到终执行的指令序列示意图
JMM 解决方案
1)Java中提供了 volatile 和 synchronized 两个关键字来保证有序性
- volatile:天然就具有有序性,禁止重排序
- synchronized和Lock:根据规则“一个变量同一时刻只允许一条线程对其进行lock操作”
2)Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性
-
as-if-seria(线程内)
- 指令重排必须保证,单线程内重排序后执行结果不变。
- 为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
-
happens-before(多线程)(JDK5,JSR-133内存模型)
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程任意后续操作
- start()规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的 start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量 的修改对线程B可见
- join()规则:Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
- volatile变量规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单 的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当 该变量发生变化时,又会强迫将新的值刷新到主内存,任何时刻,不同的线程总是能 够看到该变量的新值
- 监视器锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说, 如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)
- 传递性:如果A happens-before B,B happens-before C ,那么 A happens-before C
- 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中 断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断
- 对象终结规则:对象的构造函数执行,结束先于finalize()方法