1. Java 内存模型到底是什么玩意?
内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同架构的CPU 有不同的内存模型。
Java 作为跨平台语言,肯定要屏蔽不同CPU内存模型的差异,构造自己的内存模型,这就是Java 的内存模型。实际上,根源来自硬件的内存模型。
还是看这个图片,Java 的内存模型和硬件的内存模型几乎一样,每个线程都有自己的工作内存,类似CPU的高速缓存,而 java 的主内存相当于硬件的内存条。
Java 内存模型也是抽象了线程访问内存的过程。
JMM(Java 内存模型)规定了所有的变量都存储在主内存(这个很重要)中,包括实例字段,静态字段,和构成数据对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的。不会被共享。自然就没有竞争问题。
什么是工作内存呢?每个线程都有自己的工作内存(这个很重要),线程的工作内存保存了该线程使用到的变量和主内存副本拷贝,线程对变量的所有操作(读写)都必须在工作内存中进行。而不能直接读写主内存中的变量。不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。
总结一下,Java 内存模型定义了两个重要的东西,1.主内存,2.工作内存。每个线程的工作内存都是独立的,线程操作数据只能在工作内存中计算,然后刷入到主存。这是 Java 内存模型定义的线程基本工作方式。
2. Java 内存模型定义了哪些东西?
实际上,整个 Java 内存模型围绕了3个特征建立起来的。这三个特征是整个Java并发的基础。
原子性,可见性,有序性。
原子性(Atomicity)
什么是原子性,其实这个原子性和事务处理中的原子性定义基本是一样的。指的是一个操作是不可中断的,不可分割的。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
我们大致可以认为基本数据类型的访问读写是具备原子性的(但是,如果你在32位虚拟机上计算 long 和 double 就不一样了),因为 java 虚拟机规范中,对 long 和 double 的操作没有强制定义要原子性的,但是强烈建议使用原子性的。因此,大部分商用的虚拟机基本都实现了原子性。
如果用户需要操作一个更到的范围保证原子性,那么,Java 内存模型提供了 lock 和 unlock (这是8种内存操操作中的2种)操作来满足这种需求,但是没有提供给程序员这两个操作,提供了更抽象的 monitorenter 和 moniterexit 两个字节码指令,也就是 synchronized 关键字。因此在 synchronized 块之间的操作都是原子性的。
可见性(Visibility)
可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值,这种依赖主内存作为传递媒介的方式来实习那可见性的。无论是普通变量还是 volatile 变量都是如此。他们的区别在于:volatile 的特殊规则保证了新值能立即同步到主内存,以及每次是使用前都能从主内存刷新,因此,可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
除了 volatile 之外, synchronized 和 final 也能实现可见性。同步块的可见性是由 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存种(执行 store, write 操作)。
有序性(Ordering)
有序性这个问题我们在最上面说硬件的时候说过,CPU 会调整指令顺序,同样的 Java 虚拟机同样也会调整字节码顺序,但这种调整在单线程里时感知不到的,除非在多线程程序中,这种调整会带来一些意想不到的错误。
Java 提过了两个关键字来保证多个线程之间操作的有序性,volatile 关键字本身就包含了禁止重排序的语义,而 synchronized 则是由 “一个变量同一时刻只允许一条线程对其进行 lock 操作”这个规则获得的。这条规则决定了同一个锁的两个同步块只能串行的进入。
好了,介绍完了 JMM 的三种基本特征。不知道大家有没有发现,volatile 保证了可见性和有序性,synchronized 则3个特性都保证了,堪称万能。而且 synchronized 使用方便。但是,仍然要警惕他对性能的影响。
3. Java内存模型引出的 Happen-Before 原则是什么?
说到有序性,注意,我们说有序性可以通过 volatile 和 synchronized 来实现,但是我们不可能所有的代码都靠这两个关键字。实际上,Java 语言已对重排序或者说有序性做了规定,这些规定在虚拟机优化的时候是不能违背的。
1. 程序次序原则:一个线程内,按照程序代码顺序,书写在前面的操作先发生于书写在后面的操作。
2. volatile 规则:volatile 变量的写,先发生于读,这保证了 volatile 变量的可见性。
3. 锁规则:解锁(unlock) 必然发生在随后的加锁(lock)前。
4. 传递性:A先于B,B先于C,那么A必然先于C。
5. 线程的 start 方法先于他的每一个动作。
6. 线程的所有操作先于线程的终结。
7. 线程的中断(interrupt())先于被中断的代码。
8. 对象的构造函数,结束先于 finalize 方法。
4. Happen-Before 引出的 volatile 又是什么?
我们在前面,说了很多的 volatile 关键字,可见这个关键字非常的重要,但似乎他的使用频率比 synchronized
少多了,我们知道了这个关键字可以做什么呢?
volatile 可以实现线程的可见性,还可以实现线程的有序性。但是不能实现原子性。
我们还是直接写一段代码吧!
package cn.think.in.java.two; /** * volatile 不能保证原子性,只能遵守 hp 原则 保证单线程的有序性和可见性。 */ public class MultitudeTest { static volatile int i = 0; static class PlusTask implements Runnable { @Override public void run() { for (int j = 0; j < 10000; j++) { // plusI(); i++; } } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[10]; for (int j = 0; j < 10; j++) { threads[j] = new Thread(new PlusTask()); threads[j].start(); } for (int j = 0; j < 10; j++) { threads[j].join(); } System.out.println(i); } // static synchronized void plusI() { // i++; // } }
我们启动了10个线程分别对一个 int 变量进行 ++ 操作,注意,++ 符号不是原子的。然后,主线程等待在这10个线程上,执行结束后打印 int 值。你会发现,无论怎么运行都到不了10000,因为他不是原子的。怎么理解呢?
i++ 等于 i = i + 1;
虚拟机首先读取 i 的值,然后在 i 的基础上加1,请注意,volatile 保证了线程读取的值是最新的,当线程读取 i 的时候,该值确实是最新的,但是有10个线程都去读了,他们读到的都是最新的,并且同时加1,这些操作不违法 volatile 的定义。最终出现错误,可以说是我们使用不当。
楼主也在测试代码中加入了一个同步方法,同步方法能够保证原子性。当for循环中执行的不是i++,而是 plusI 方法,那么结果就会准确了。
volatile 确实不能保证原子性,但是能保证有序性和可见性。那么是怎么实现的呢?
怎么保证有序性呢?实际上,在操作 volatile 关键字变量前后的汇编代码中,会有一个 lock 前缀,根据 intel IA32 手册,lock 的作用是 使得 本 CPU 的Cache 写入了内存,该写入动作也会引起别的CPU或者别的内核无效化其Cache,别的CPU需要重新获取Cache。这样就实现了可见性。可见底层还是使用的 CPU 的指令。
如何实现有序性呢?同样是lock 指令,这个指令还相当于一个内存屏障(大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障),指的是,重排序时不能把后面的指令重排序到内存屏障之前的位置。只有一个CPU访问内存时,并不需要内存屏障;但如果有两个或者更多CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证了。
因此请不要随意使用 volatile 变量,这会导致 JIT 无法优化代码,并且会插入很多的内存屏障指令,降低性能。
5.使用volatile关键字的场景
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } }
至于为何需要这么写请参考:《Java 中的双重检查(Double-Check)》 http://www.iteye.com/topic/652440
6. 总结
首先 JMM 是抽象化了硬件的内存模型(使用了多级缓存导致出现缓存一致性协议),屏蔽了各个 CPU 和操作系统的差异。
Java 内存模型指的是:在特定的协议下对内存的访问过程。也就是线程的工作内存和主存直接的操作顺序。
JMM 主要围绕着原子性,可见性,有序性来设置规范。
synchronized 可以实现这3个功能,而 volatile 只能实现可见性和有序性。final 也能是实现可见性。
Happen-Before 原则规定了哪些是虚拟机不能重排序的,其中包括了锁的规定,volatile 变量的读与写规定。
而 volatile 我们也说了,不能保证原子性,所以使用的时候需要注意。volatile 底层的实现还是 CPU 的 lock 指令,通过刷新其余的CPU 的Cache 保证可见性,通过内存栅栏保证了有序性。
volatile关键字内存栅栏有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
下面这篇文章从实战角度专门探索 volatile 关键字原理
并发编程之volatile 关键字白话文解读_击水三千里的专栏-CSDN博客