(2.1.27.2)Java并发编程:JAVA的内存模型

文章目录

  1. Java定义了自身的内存模型是为了屏蔽掉不同硬件和操作系统的内存模型差异
  2. Java为了处理内存的不可见性与重排序的问题,定义了Happens-Before 原则
  3. Happens-Before 原则的理解:对于两个操作A和B(这两个操作可以在不同的线程中执行),如果A Happens-Before B,那么可以保证,当A操作执行完后,A操作的执行结果对B操作是可见的。

通过阅读上一章,我们知道了[缓存模型]和[乱序执行]带来的问题,我们所讨论的CPU高速缓存、指令重排序等内容都是计算机体系结构方面的东西,并不是Java语言所特有的。
事实上,很多主流程序语言(如C/C++)都存在缓存不一致的问题,这些语言是借助物理硬件和操作系统的内存模型来处理缓存不一致问题的,因此不同平台上内存模型的差异,会影响到程序的执行结果。
Java虚拟机规范定义了自己的内存模型JMM(Java Memory Model)来屏蔽掉不同硬件和操作系统的内存模型差异,以实现让Java程序在各种平台下都能达到一致的内存访问结果。
所以对于Java程序员,无需了解底层硬件和操作系统内存模型的知识,只要关注Java自己的内存模型,就能够解决这些问题啦。

一、Java的内存模型

《(2.1.27.2)Java并发编程:JAVA的内存模型》
【Java内存模型】

  • 主内存
    • 主要存储变量(包括。实例字段,静态字段和构成对象的元素)
    • 对应Java内存中的堆
  • 工作内存
    • 每个线程都有自己的工作内存,存储了对应的引用,方法参数。
    • 对应Java虚拟机的栈

二、工作内存和主内存的交互

主内存与工作内存之间的内存交互,可以分为两种,也就是:

  1. 从主内存的读取数据到线程的私有内存中
  2. 从线程的私有内存数据同步到主内存中

Java内存模型定义了8种操作来完成。虚拟机在实现时保证下面提到的每一种操作都是原子的,不可再分的

《(2.1.27.2)Java并发编程:JAVA的内存模型》
【工作内存和主内存的交互】

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

2.1 八种原子操作规则

java为了保证数据在单线程情形下传输过程中的准确性与数据一致性,规定了内存之间交互的一些操作规则

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

三、Java内存模型的需要解决的问题

前面我们已经了解了Java内存模型的大致结构与操作方式,那么我们来看看Java内存模型需要解决的问题。

3.1 工作内存的可见性问题

工作内存的可见性问题 == 计算机硬件的缓存不一致问题

当多个线程操作同一个共享变量时,如果一个线程修改了其中的变量的值(如果通过Java内存模型的原子操作来表达,一个线程多次use与assign 操作,而另一个线程经过read、load之后,另一线程任然保持着之前从主内存中获取的值),另一个线程怎么感知呢?

3.2 重排序在多线程中引发的问题

虽然重排序规则(as-if-serial)保证了单线程模型中的执行结果一致性,但是CPU(处理器)重排序在多线程模型中依然存在问题。具体问题我们用下列伪代码来阐述:

public class Demo {
    private int a = 0;
    private boolean isInit = false;//标志是否已经初始化配置
    private Config config;

    public void init() {
        config = readConfig();//1
        isInit = true;//2
    }
	
    public void doSomething() {
        if (isInit) {//3
            doSomethingWithconfig();//4
        }
    }
}

其中1-2,3-4操作是没有数据依赖性的。也就是说把1-2,3-4的调换顺序对于单线程中的执行结果是没有影响的。 那么CPU(处理器)可能对1-2操作进行重排序,对3-4操作进行重排序。

现在我们加入线程A操作Init()方法,线程B操作doSomething()方法,那么我们看看重排序对多线程情况下的影响:

《(2.1.27.2)Java并发编程:JAVA的内存模型》
【重排序带来的问题】

上图中2操作排在了1操作前面。当CPU时间片转到线程B。线程B判断 if (isInit)为true,接下来接着执行 doSomethingWithconfig(),但是实际上,我们Config还没有初始化。

所以在多线程的情况下。重排序会影响程序的执行结果。

四、Happens-Before 原则

上面我们讨论了Java内存模型需要解决的问题,那Java有不有一个良好的解决办法来处理以上出现的情况呢?答案是当然的。

为了方便程序员开发,将底层的烦琐细节屏蔽掉,JMM定义了Happens-Before原则。只要我们理解了Happens-Before原则,无需了解Java内存模型的内存操作,就可以解决这些问题(避免工作内存的不可见与重排序带来的问题)。

Happens-Before原则是一组偏序关系:对于两个操作A和B(这两个操作可以在不同的线程中执行), 如果A Happens-Before B,那么可以保证,当A操作执行完后,A操作的执行结果对B操作是可见的。

那么有哪些满足Happens-Before原则的呢?下面是Java内存模型规定的一些规则

4.1 程序次序规则

在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。

这是因为Java语言规范要求Java内存模型在单个线程内部要维护类似严格串行的语义,如果多个操作之间有先后依赖关系,则不允许对这些操作进行重排序。

4.2 锁定规则

对一个unlock操作先行发生于后面对同一个锁的lock操作。

public class Demo {
    private int value;
    public synchronized void setValue(int value) {
        this.value = value;
    }
    public synchronized int getValue() {
        return value;
    }
}

上面这段代码,setValue与getValue拥有同一个锁(也就是当前实例对象).

假设setValue方法在线程A中执行,getValue方法在线程B中执行。

线程A调用setValue方法会先对value变量赋值,然后释放锁。线程B调用getValue方法会先获取到同一个锁后,再读取value的值。那么B线程获取的value的值一定是正确的。

4.3 volatlie变量规则

对一个volatile变量的写操作先行发生于后面这个变量的读操作。

public class Demo {

    private volatile boolean flag;
	
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
	
    public boolean isFlag() {
        return flag;
    }
}

假设setFlag方法在线程A中执行,isFlag方法在线程B中执行:

线程A调用setFlag方法会先对value变量赋值,然后释放锁。线程B调用isFlag方法再读取value的值。那么B线程获取的flag的值一定是正确的。

4.4 线程启动规则

Thread对象的start()方法先行发生于此线程的每个动作。

start方法和新线程中的动作一定是在两个不同的线程中执行。

线程启动规则可以这样去理解:调用start方法时,会将start方法之前所有操作的结果同步到主内存中,新线程创建好后,需要从主内存获取数据。这样在start方法调用之前的所有操作结果对于新创建的线程都是可见的。

4.5 线程终止规则

线程中的所有操作都先行发生于对此线程的终止检测。

这里理解比较抽象。举个例子,假设两个线程s、t。

  1. 在线程s中调用t.join()方法。则线程s会被挂起,等待t线程运行结束才能恢复执行。
  2. 当t.join()成功返回时,s线程就知道t线程已经结束了。
  3. 在t线程中对共享变量的修改,对s线程都是可见的。

类似的还有Thread.isAlive方法也可以检测到一个线程是否结束。也就是说当一个线程结束时,会把自己所有操作的结果都同步到主内存。而任何其它线程当发现这个线程已经执行结束了,就会从主内存中重新刷新最新的变量值。所以结束的线程A对共享变量的修改,对于其它检测了A线程是否结束的线程是可见的。

4.6 线程中断规则

对线程interrupt()方法的调用先与被中断线程的代码检查到中断事件的发生。

假设两个线程A和B,A先做了一些操作operationA,然后调用B线程的interrupt方法。当B线程感知到自己的中断标识被设置时(通过抛出InterruptedException,或调用interrupted和isInterrupted),operationA中的操作结果对B都是可见的。

4.7 对象终结规则

一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

4.8 传递性规则

如果操作A先行与发生于操作B,操作B先行发生于操作C,那么就可以得出A先行发生于操作C的结论。

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/fei20121106/article/details/83186171
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞