Java 虚拟机在规范中视图定义一种 Java 内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
Java 内存模型的主要目标是定义程序中各个变量的访问规则,此处的变量(Variables)包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的。 Java 内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程都拥有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需通过主内存来完成,线程、主内存、工作内存三者的交互关系如下图所示:
关于一个变量如何如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之类的实现细节,Java 内存模型中定义了以下 8 种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
如果要把一个变量从主内存复制到工作内存,那就要顺序地执行 read 和 load 操作,如果要把变量从工作内存同步回主内存,就要顺序地执行 store 和 write 操作。这两组操作都只需要顺序执行而不必连续。除此之外,Java 内存模型还规定了在执行上述 8 种基本操作时必须满足如下规则:
关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制,了解 volatile 变量的语义对后面了解多线程操作的其它特性很有意义。首先,我们来了解一下这个关键字的作用。 当一个变量定义为 volatile 之后,它将具备两种属性,第一种是保证此变量对所有线程的可见性,这里的 "可见性" 是指当一条线程修改了这个变量的值,新值对于其它线程来说是可以立即得知的。不过,因为 Java 里面的运算并不是原子操作,所以 volatile 变量的运算在并发条件下并不是绝对安全的。以下为示例代码:
- packagecom.overridere.twelve;/**
- * volatile变量自增运算测试
- */
- public class VolatileTest{
- public static volatile intrace =0;public static void increase() {
- race++;
- }private static final intTHREADS_COUNT =20;public static void main(String[] args) {
- Thread[] threads =newThread[THREADS_COUNT];for(inti =0; i < THREADS_COUNT; i++) {
- threads[i] =newThread(newRunnable() {@Override
- public void run() {for(inti =0; i <10000; i++) {
- increase();
- }
- }
- });
- threads[i].start();
- }// 等待所有累加线程都结束
- while(Thread.activeCount() >1)
- Thread.yield();
- System.out.println(race);
- }
- }
这段代码发起了 20 个线程,每个线程都对变量 race 进行 10000 次自增操作,如果 race 是线程安全的,最后输出结果应该是 200000,但真实结果都是一个小于 200000 的值,这是问为什么呢?
- public static void increase();
- descriptor: ()V
- flags: ACC_PUBLIC, ACC_STATIC
- Code:
- stack=2, locals=0, args_size=0
- 0: getstatic #13 // Field race:I
- 3: iconst_14: iadd5: putstatic #13 // Field race:I
- 8:returnLineNumberTable:
- line10:0line11:8LocalVariableTable:
- Start Length Slot Name Signature
问题就在 "race++" 中,用 javap 命令得到上面的字节码命令,发现只有一行代码的 increase() 方法在 Class 文件中是由 4 条字节码指令构成的,从这上面分析就很容易分析出线程不安全的原因了:当 getstatic 指令把 race 的值取到操作数栈顶的时候,volatile 关键字保证了 race 的值在此时是正确的,但是在执行 iconst_1、iadd 这些指令的时候,其它线程可能已经把 race 加大了,而操作栈顶的值就变成了过期的数据。意思是说,在这个例子当中,volatile 关键字只保证 getstatic 指令从常量池中将值取出放到操作数栈顶这个动作是线程安全的(从主内存到工作内存),作用范围就是这个,之后就会失去作用,后面的增加操作就不是线程安全的了。
第一句话的意思是:不管我以前是什么样的,都不影响现在的我。 第二句话的意思是:不管别人是什么样的,只要跟我没关系,就不会影响到我。 如下面的示例代码:
- volatile booleanisVolatile;public void shutdown(){
- isVolatile =true;
- }public void doWork(){while(!isVolatile){//do stuff}
- }
上面代码中的 isVolatile 变量,无论之前是什么,都不妨碍当前线程赋值,也没有与其它变量绑起来共同参与不变约束。
使用 volatile 变量的第二个语义是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,也就是 Java 内存模型中描述的所谓 "线程内表现为串行的语义"(Within-Thread As-If-Serial Semantics)。
指令重排序意思就是不按程序规定的顺序执行指令,但并不是说指令任意重排。比如说,指令 1 把地址 A 中的值加 10,指令 2 把地址 A 中的值乘以 2,指令 3 把地址 B 中的值减去 3,这时指令 1 和指令 2 是有依赖的,它们之间的顺序不能重排——(A+10)*2 与 A*2+10 显然不相等,但是指令 3 可以重排到指令 1、2 之前或者中间。
而 valotile 关键字是如何实现禁止指令重排序呢? 有 volatile 关键字修饰的变量,赋值后会多执行一个操作,这个操作会把修改同步到内存,意味着所有之前的操作都执行完毕了,这个操作相当于内存屏障(Memory Barrier),这样一来指令重排序的时候就不能把后面的指令重排序到内存屏障之前的位置。只有一个 CPU 访问内存时,不需要内存屏障。
原子性(Atomicity):由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、和 write。 可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。volatile 变量与普通变量的区别是,volatile 的特殊规则保证新值能立即同步到主内存,每次使用前立即从主内存刷新。 有序性(Ordering):如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指 "线程内表现为串行的语义",后半句是指 "指令重排序" 现象和 "工作内存和主内存同步延迟" 现象。
先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,如果说操作 A 先行发生于操作 B,意思就是在发生操作 B 之前,操作 A 产生的影响能被操作 B 观察到。请看下面的例子:
- //以下操作在线程A中执行i =1;//以下操作在线程B中执行j = i;//以下操作在线程C中执行i =2;
假设线程 A 中的操作先行发生于线程 B 的操作,name 可以确定在线程 B 的操作执行之后,变量 j 的值一定等于 i,依据有两个:以是根据现行发生原则,"i=1" 的结果可以被线程 B 观察到;二是线程 C 还没执行,线程 A 操作结束之后没有其它线程会修改变量 i 的值。 再把问题变一下,我们依然保持线程 A 和线程 B 的先行发生关系,而线程 C 出现在线程 A 和线程 B 之间,但是线程 C 和线程 B 没有先行发生关系,那么 j 的值会是多少呢?答案是不确定,因为线程 C 和线程 B 不确定哪条线程先执行到对变量 j 的修改。
来源: http://blog.csdn.net/nicorui/article/details/70187447