普通内存模型
当程序运行时,会将计算需要的变量从主内存copy一份到cpu的高速缓存区,计算的时候直接从高速缓存读取数据和向其写入数据,当运行结束后再将高速缓存区的数据刷新到主内存中。
例如:i=i+1
执行过程:当线程执行这个语句时,会先从主内存中将i复制到高速缓存区,cpu执行指令+1,然后将结果写入高速缓存区,最后将高速缓存区的值刷新到主内存中。
单线程不会出现错误。
多线程时,可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。
为了解决缓存不一致性问题,通常来说有以下2种解决方法:
1)通过在总线加LOCK#锁的方式
因为cpu与其他部件通信都通过总线,给总线加lock锁,阻塞其他cpu对内存访问,这样等待cpu将这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量进行。缺点:效率低下
2)通过缓存一致性协议
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
这2种方式都是硬件层面上提供的方式。用它本地寄存器的值进行操作,造成数据的不一致。
Java内存模型
Java内存模型定义:
Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。
Java内存模型目标:
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量主要是指共享变量,存在竞争问题的变量。
当前的java内存模型下,所有的变量都存储在主内存中,每条线程拥有各自的工作内存,可以将变量保存在本地内存(机器的寄存器)中,变量的读取、赋值操作都必须在工作内存中进行,不用直接读取主内存的变量。
注:不同线程之间不能互相访问工作内存,线程间变量的传递只能通过主内存。
例如i=10
执行过程必须先在自己的工作内存中对变量i所在的缓存行赋值10,然后再写入到主内存中。
此内存模型会导致:这就可能造成一个线程在主存中修改了一个变量的值,而另一个线程还继续使用其工作内存的缓存值,造成不一致。
happens-before原则
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
下面就来具体介绍下happens-before原则(先行发生原则):
1) 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
2) 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
3) volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
4) 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
5) 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
6) 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
7) 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
8) 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
这8条原则摘自《深入理解Java虚拟机》。
这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。
下面我们来解释一下前4条规则:
程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果处于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。
第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。
第四条规则实际上就是体现happens-before原则具备传递性。
指令重排序:java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
指令重排序的意义:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
Volatile变量的特殊规则
Volatile关键字修饰的变量(共享变量–类成员变量、类静态成员变量),在多线程情况下,如果该变量发生改变,会强制将变量立即写回主内存,其会使其他线程中对应的变量缓存行无效,即要求其他线程必须从主内存中读取最新值。
Volatile变量具有两层语义:
(1)保证了不同线程对该变量进行操作时的可见性。即一个线程修改该变量的值,其他线程是立即可见该变量的新值。
(2)禁止进行指令重排序(原理:确保指令重排序时不会把后面指令排列到内存屏障(内存栅栏:在volatile变量上加lock前缀指令)之前的位置,并且不会把变量之前的指令排到内存屏障之后的位置,即在执行内存屏障这句话时,这前面的操作已全部完成。)
Final域的特殊规则
Final类型是不可修改的,在java内存模型中,final能确保初始化过程的安全性,即在构造器中final修饰的变量一旦被初始化,并且未引入把this的引入传递出去,那么其他线程可共享此final变量,而且其外、外部可见状态永远不会改变。例如:单例模式中static final
long和double型变量的特殊规则
Java内存模型要求lock、unlock、read、load、assign、use、store和write这8个操作都具有原子性,但是对于64位的数据类型long和double,在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行。在编码时,不需要将long和double变量专门声明为volatile。