Java内存模型

一、Java内存模型概念

  硬件系统是通过基于高速缓存的存储交互方法解决了处理器与内存的速度矛盾,但这也带来了缓存一致性问题。而Java内存模型是用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的内存访问效果。

  Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(此处的变量不是一般的变量,其包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的。)存储到内存和从内存中取出变量这样的底层细节。

  Java内存模型规定了所有的变量都存储在主内存(Main Memory,可以与物理硬件中的主内存对应)中。每条线程还有自己的工作内存(Working Memory,可以与处理器高速缓存相对应),线程的工作内存中保存了从主内存拷贝的该线程使用到的变量的副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问其它线程的工作内存中的变量,线程间变量值的传递必须通过主内存来完成。线程、主内存、工作内存的交互关系如下图:


二、原子性、可见性与有序性

  Java内存模型就是为了处理并发编程过程中的原子性、可见性和有序性三个特性而建立的。

1、原子性

  即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在Java中,我们可以认为基本数据类型的访问读写是具有原子性的(例外就是long和double的非原子性协定)。

[java]  view plain  copy
  1. x = 10;         //语句1  
  2. y = x;         //语句2  

  语句1是原子性操作,线程执行这个语句的会直接将数值10写入到工作内存中。语句2不是原子性操作,其包含两个操作,读取x的值和将值写入到工作内存中,虽然单独的两个操作都是原子性的,但是合在一起就不是了。

2、可见性

  可见性是指一个线程修改了一个变量的值,其他线程能够立即得知这个修改的值。

[java]  view plain  copy
  1. //线程1执行的代码  
  2. int i = 0;  
  3. i = 10;  
  4.    
  5. //线程2执行的代码  
  6. j = i;  

  当线程1执行 i =10这句时,会先把i的初始值加载到线程1的工作内存中,然后赋值为10,那么在线程1的工作内存中i的值变为10了,却没有立即写入到主内存中。

  此时线程2执行 j = i,它会先去主内存读取i的值并加载到线程2的工作内存中,注意此时主内存当中i的值还是0,那么就会使得j的值为0,而不是10。

  这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

3、有序性

  即程序执行的顺序按照代码的先后顺序执行。Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。

  在这里就会牵扯到指令重排序的问题,解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

  虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

  指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

三、Java内存模型如何保证原子性、可见性与有序性

  原子性:Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

  可见性:对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

  有序性:在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

四、volatile型变量的特殊规则

1、volatile保证变量的可见性

  volatile 保证了不同线程对变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  volatile关键字保证了操作的可见性,但是volatile不能保证对变量的操作是原子性。看如下例子:

[java]  view plain  copy
  1. public class Test {  
  2.     public volatile int inc = 0;  
  3.        
  4.     public void increase() {  
  5.         inc++;  
  6.     }  
  7.        
  8.     public static void main(String[] args) {  
  9.         final Test test = new Test();  
  10.         for(int i=0;i<10;i++){  
  11.             new Thread(){  
  12.                 public void run() {  
  13.                     for(int j=0;j<1000;j++)  
  14.                         test.increase();  
  15.                 };  
  16.             }.start();  
  17.         }  
  18.            
  19.         while(Thread.activeCount()>1)  //保证前面的线程都执行完  
  20.             Thread.yield();  
  21.         System.out.println(test.inc);  
  22.     }  
  23. }  

  如果这段代码能够正确并发执行的话,最后输出的应该是10000。但是运行这段代码会发现,每次运行的结果都不一样,而且是一个小于10000的数字。

  可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。

  假如某个时刻变量inc的值为10,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,将inc的值10放入到线程1的操作数栈的栈顶,然后线程1被阻塞了;然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去工作内存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。然后线程1接着进行加1操作,由于线程1的操作数栈顶已经存在inc的值10了,所以线程1不需要再去工作内存读取了,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。那么两个线程分别进行了一次自增操作后,inc只增加了1。

  根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

  可以使用synchronized对increase()方法进行加锁,这样的话就能正确并发了。

[java]  view plain  copy
  1. public class Test {  
  2.     public  int inc = 0;  
  3.       
  4.     public synchronized void increase() {  
  5.         inc++;  
  6.     }  
  7.       
  8.     public static void main(String[] args) {  
  9.         final Test test = new Test();  
  10.         for(int i=0;i<10;i++){  
  11.             new Thread(){  
  12.                 public void run() {  
  13.                     for(int j=0;j<1000;j++)  
  14.                         test.increase();  
  15.                 };  
  16.             }.start();  
  17.         }  
  18.           
  19.         while(Thread.activeCount()>1)  //保证前面的线程都执行完  
  20.             Thread.yield();  
  21.         System.out.println(test.inc);  
  22.     }  
  23. }  

2、volatile进制指令重排序

  volatile关键字禁止指令重排序有两层意思:
  1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

[java]  view plain  copy
  1. //x、y为非volatile变量  
  2. //flag为volatile变量  
  3.    
  4. x = 2;        //语句1  
  5. y = 0;        //语句2  
  6. flag = true;  //语句3  
  7. x = 4;         //语句4  
  8. y = -1;       //语句5  

  由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

  并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

3、volatile的实现机制

  关键变化在于有volatile修饰的变量赋值后多执行了一个lock操作,这个操作相当于一个内存屏障,指重排序时不能把后面的指令重排序到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;它会强制将对缓存的修改操作立即写入主存;如果是写操作,它会导致其他线程工作内存中对应的缓存无效。

五、先行发生原则

  Java内存模型中存在着一些“天然的”先行发生关系,这些先行发生关系无需任何同步器的协助,可以在编码中直接使用。如果两个操作之间的关系不在这些情况之中,并且无法从下列规则中推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。

  先行发生原则:

    程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
    锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作。
    volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
    传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
    线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。
    线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
    线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
    对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值