Java内存模型与线程

来源:互联网 发布:大数据的4v特征变化快 编辑:程序博客网 时间:2024/06/05 18:39

Java内存模型

Java虚拟机在规范中视图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

主内存与工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,此处的变量(Variables)包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程都拥有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需通过主内存来完成,线程、主内存、工作内存三者的交互关系如下图所示:
这里写图片描述

内存间交互操作

关于一个变量如何如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。

  • lock(锁定):作用于主内存变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。
  • read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便write操作使用。
  • write(写入):作用于主内存变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

如果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作。这两组操作都只需要顺序执行而不必连续。除此之外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:

  • 不允许你read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
  • 不允许一个线程丢弃它的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 不允许一个线程无原因地(没有发生过任何assign)把数据从线程的工作内存同步到主内存。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,单lock操作可以被同一条线程重复执行多次,多次执行lock之后,只有执行相同次数的unlock操作,变量才会被解锁。
  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其它线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(只是sotore、write操作)。

volatile关键字

关键字volatile是Java虚拟机提供的最轻量级的同步机制,了解volatile变量的语义对后面了解多线程操作的其它特性很有意义。首先,我们来了解一下这个关键字的作用。
当一个变量定义为volatile之后,它将具备两种属性,第一种是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其它线程来说是可以立即得知的。不过,因为Java里面的运算并不是原子操作,所以volatile变量的运算在并发条件下并不是绝对安全的。以下为示例代码:

package com.overridere.twelve;/** * volatile变量自增运算测试 */public class VolatileTest {    public static volatile int race = 0;    public static void increase() {        race++;    }    private static final int THREADS_COUNT = 20;    public static void main(String[] args) {        Thread[] threads = new Thread[THREADS_COUNT];        for (int i = 0; i < THREADS_COUNT; i++) {            threads[i] = new Thread(new Runnable() {                @Override                public void run() {                    for (int i = 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_1         4: iadd         5: putstatic     #13                 // Field race:I         8: return      LineNumberTable:        line 10: 0        line 11: 8      LocalVariableTable:        Start  Length  Slot  Name   Signature

问题就在“race++”中,用javap命令得到上面的字节码命令,发现只有一行代码的increase()方法在Class文件中是由4条字节码指令构成的,从这上面分析就很容易分析出线程不安全的原因了:当getstatic指令把race的值取到操作数栈顶的时候,volatile关键字保证了race的值在此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其它线程可能已经把race加大了,而操作栈顶的值就变成了过期的数据。意思是说,在这个例子当中,volatile关键字只保证getstatic指令从常量池中将值取出放到操作数栈顶这个动作是线程安全的(从主内存到工作内存),作用范围就是这个,之后就会失去作用,后面的增加操作就不是线程安全的了。

  • 运算结果并不依赖变量的当前值,或者能够保证只有单一的线程修改变量的值。
  • 变量不需要与其它的变量共同参与不变约束。

第一句话的意思是:不管我以前是什么样的,都不影响现在的我。
第二句话的意思是:不管别人是什么样的,只要跟我没关系,就不会影响到我。
如下面的示例代码:

volatile boolean isVolatile;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的修改。

2 0
原创粉丝点击