4.9--Java多线程之Java内存模型+volatile总结--(复习+总结篇)---一步一个脚印

Posted 奔赴在自己的热爱中

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了4.9--Java多线程之Java内存模型+volatile总结--(复习+总结篇)---一步一个脚印相关的知识,希望对你有一定的参考价值。

1.什么是 JMM 内存模型:
Java 内存模型是 Java 虚拟机定义的一种多线程访问 Java 内存各个变量的访问规范,主要围绕如何解决并发过程中的原子性、可见性、有序性这三个问题来解决线程的安全问题

Java 内存模型将内存分为了主内存和工作内存(也称为栈空间)。
主内存存放所有的共享变量,所有线程都可以访问。
每个线程都有自己的工作内存,存储了该线程使用到的变量的副本,线程对变量的所有操作都必须在自己的工作内存中完成,不能直接操作主存中的变量。
操作时,首先将变量从主内存拷贝到自己的工作内存中,然后在自己的工作内存中对变量进行操作,操作完成后再将变量写回主存。
不同的线程间也无法直接访问对方的工作内存的变量,线程间的变量值的传递必须通过主内存来完成。

1.原子性:原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
2.可见性:可见性指的是,当一个线程修改了某个共享变量的值,其他线程能够马上得知这个修改的值。

串行程序不存在可见性问题,因为在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个修改过的变量值。
但在多线程环境中可就不一定了,因为线程对共享变量的操作都是拷贝到各自的工作内存中进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题

3.有序性:对于多线程环境,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,有可能出现乱序现象

指令重排序:计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,在单线程条件下,指令重排序可以保证执行结果的一致性,但是在多线程条件下,这些重排优化可能会导致程序出现内存可见性问题,不能保证多线程间语义一致性

2、原子性、可见性、有序性问题的解决措施:

1.原子性问题:除了 JVM 自身提供对基本数据类型读写操作的原子性外,对于方法级别或者代码级别的原子性操作,可以使用 synchronized 关键字或者重入锁 ReentrantLock 保证程序执行的原子性。

2.可见性问题:工作内存与主内存同步延迟现象导致的可见性问题,可以使用 synchronized 关键字、Lock 或者 volatile 关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

3.有序性问题:可以利用 volatile、synchronized 关键字解决。

3、JMM 的 as-if-serial 规则和 happens-before 规则:

1.as-if-serial:无论编译器和处理器如何进行重排序,单线程程序的执行结果不会改变。

2.happens-before:在多线程程序开发中,如果仅靠 synchronized 和 volatile 关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,所以 Java 内存模型中提供了内置的 happens-before 规则来辅助处理前面提到的问题,它是判断数据是否存在竞争、线程是否安全的依据,从而保证线程安全。

一个操作 happens-before 另一个操作,表示第一个的操作结果对第二个操作可见,并且第一个操作的执行顺序也在第二个操作之前。
但这并不意味着 JVM 必须按照这个顺序来执行程序。如果重排序后的执行结果与按 happens-before 关系执行的结果一致,JVM 也允许重排序的发生。
happens-before 原则内容如下:

1.程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
2.锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
3.volatile 规则:volatile 变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile 变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
4.线程启动规则:线程的 start() 方法先于它的每一个动作,即如果线程A在执行线程B的 start() 方法之前修改了共享变量的值,那么当线程B执行 start() 方法时线程A对共享变量的修改对线程B可见。
5.传递性:A先于B ,B先于C,那么A必然先于C
6.线程终止规则:线程的所有操作先于线程的终结,Thread.join() 方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程从线程B的 join() 方法成功返回后,线程B对共享变量的修改将对线程A可见。
7.线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测线程是否中断。
8.对象终结规则:对象的构造函数执行,结束先于 finalize() 方法。

二、volatile 内存语义:

1、volatile 的作用:

volatile 是 Java 虚拟机提供的轻量级同步机制,是线程不安全的,volatile 跟可见性和有序性有关,被 volatile 修饰的共享变量,具有以下两个作用:

1.保证不同线程对该变量操作的内存可见性:当变量被 volatile 修饰时,那么对它的修改会立刻刷新到主存,同时其他线程操作 volatile 变量时,JMM 会把该线程对应的工作内存置为无效,那么该线程就只能从主存中重新读取共享变量,保证读取到最新值

通过 synchronized 和 Lock 也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是相比于 volatile,synchronized 和 Lock 的开销都更大

2.禁止指令重排序

2、内存屏障:

把加入 volatile 关键字的代码和未加入 volatile 关键字的代码都生成汇编代码,会发现加入 volatile 关键字的代码会多出一个内存屏障指令,它是一个 CPU 指令

JMM是如何实现让volatile变量对其他线程立即可见的呢?

当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重新读取共享变量。volatile变量正是通过这种写-读方式实现对其他线程可见(但其内存语义实现则是通过内存屏障)

内存屏障提供了以下功能:

1.告诉编译器和处理器,重排序时不能把后面的指令重排序到内存屏障之前的位置,从而避免多线程环境下出现乱序执行现象

2.保存某些变量的内存可见性,利用该特性实现 volatile 的内存可见性

3、volatile 的原子性:
volatile 的两点内存语义能保证可见性和有序性,但是不能保证原子性:对单个 volatile 变量的读/写具有原子性,但是对于类似 volatile++ 这样的复合操作就无能为力了

要想保证原子性,只能借助于 synchronized、Lock 或者并发包下的 atomic 的原子操作类

4、volatile使用场景:
(1)状态量标记:这种对变量的读写操作,标记为 volatile 可以保证修改对线程立刻可见,效率也比 synchronized、Lock 有一定的提升。

public class VolatileSafe 
​
    volatile boolean close;
​
    public void close()
        close=true;
    
​
    public void doWork()
        while (!close)
            System.out.println("safe....");
        
    

对于 boolean 变量 close 值的修改属于原子性操作,因此可以通过使用 volatile 修饰变量 close,使用该变量对其他线程立即可见,从而达到线程安全的目的。

(2)单例模式的实现,典型的双重检查锁定(DCL):

这是一种懒汉的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排序,给 instance 加上了 volatile。指令重排序会导致,当一条线程访问的 instance 不为 null 时,但是实际上 instance 实例未必已初始化完成,也就造成了线程安全问题

public class DoubleCheckLock 
    //禁止指令重排优化
    private volatile static DoubleCheckLock instance;
​
    private DoubleCheckLock()
    public static DoubleCheckLock getInstance()
​
        //第一次检测
        if (instance==null)
            //同步
            synchronized (DoubleCheckLock.class)
                if (instance == null)
                    //多线程环境下可能会出现问题的地方
                    instance = new DoubleCheckLock();
                
            
        
        return instance;
    

上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

memory = allocate(); //1.分配对象内存空间
instance(memory);    //2.初始化对象
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null

由于步骤2和步骤3间可能会重排序,如下:

memory = allocate(); //1.分配对象内存空间
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);    //2.初始化对象

使用volatile禁止instance变量被执行指令重排优化即可。

  //禁止指令重排优化
  private volatile static DoubleCheckLock instance;

单例模式的线程安全性:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法:
(1)饿汉式单例模式的写法:线程安全
(2)懒汉式单例模式的写法:非线程安全
(3)双检锁单例模式的写法:线程安全

华为面试官问我关于“多线程之内存模型”,看完这篇,自信满满!

多线程之内存模型

首先看一下整体概览图

我们常说的JVM内存模式指的是JVM的内存分区;而Java内存模式是一种虚拟机规范。Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异(不同操作系统对缓存一致性的实现有所不同),为了实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程何时可以看到由其他线程修改过后的共享变量的值以及如何同步访问共享变量。

线程同步:从百度百科上查询到的资料:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。

Java内存模型(不仅仅是JVM内存分区):调用栈和本地变量存放在线程栈上,对象存放在堆上。

先看下在一个JVM进程中的线程栈的分布:

再看一下操作系统CPU和内存之间的关系:

CPU在操作内存的时候并不是直接进行操作的。而是从CPU到CPU的三级缓存然后再到主存中。JVM运行的时候会将数据放入到主内中。

所以操作系统在执行的时候,会加载JVM运行的数据来将数据取出来来进行实现。

  • 一个本地变量可能是基本类型,在这种情况下,它总是“呆在”线程栈上。
  • 一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。
  • 一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量仍然存放在线程栈上,即使这些方法所属的对象存放在堆上。
  • 一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。
  • 静态成员变量跟随着类定义一起也存放在堆上。
  • 存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个成员变量的私有拷贝。

对于最后一个情况的就是用lock锁来解释是最恰当的,因为使用了volitile关键字来修饰的,对所有线程来说,都是可见的。

其实对于不加volitile关键字的变量来说,在进行改变之后,其实也是可以发生改变的,但是是否是及时的修改并不一定,所以这就造成可能出现问题。而我们来进行写程序就是为了避免这些可能出现问题的地方。

硬件内存架构

现代硬件内存模型与Java内存模型有一些不同,理解内存模型架构以及Java内存模型如何与它协同工作也是非常重要的。

现代计算机硬件架构的简单图示:

  • 多CPU:一个现代计算机通常由两个或者多个CPU。其中一些CPU还有多核。从这一点可以看出,在一个有两个或者多个CPU的现代计算机上同时运行多个线程是可能的。每个CPU在某一时刻运行一个线程是没有问题的。这意味着,如果你的Java程序是多线程的,在你的Java程序中每个CPU上一个线程可能同时(并发)执行。
  • CPU寄存器:每个CPU都包含一系列的寄存器,它们是CPU操作内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存。CPU直接访问内存和CPU直接访问寄存器在速率上可能相差了100个数量级。
  • 高速缓存cache:由于计算机的存储设备与处理器(CPU)的运算速度之间有着几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。CPU访问缓存层的速度快于CPU直接访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。每个CPU可能有一个CPU缓存层,一些CPU还有多层缓存。在某一时刻,一个或者多个缓存行(cache lines)可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。
  • 内存:一个计算机还包含一个主存。所有的CPU都可以访问主存。主存通常比CPU中的缓存大得多。
  • 运作原理:通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

对于这里的运作原理和磁盘预读性以及局部性原理做了一定的实现。我们可以理解成早期计算机CPU是直接访问磁盘的,但是发现效率太慢了,然后加了一层缓存,这块缓存称之为内存;然后发现CPU访问内存也太慢了,又加入了内存;再后来发现还是太慢了,又加入了CPU缓存。

一些问题:(多线程环境下尤其)

缓存一致性问题:在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等等:不同的操作系统对其做了实现。这也是之前在开头提到的屏蔽掉各种硬件和操作系统的内存访问差异

指令重排序问题:为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化

Java内存模型和硬件内存架构之间的桥接

Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内存中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。如下图所示:

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:

  • 线程之间的共享变量存储在主内存(Main Memory)中
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
  • 从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
  • Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。

JMM模型下的线程间通信:

线程间通信必须要经过主内存。

如下,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤:

1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。

2)线程B到主内存中去读取线程A之前已更新过的共享变量。

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。

  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。

  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

    Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

    • 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
    • 不允许read和load、store和write操作之一单独出现
    • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
    • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
    • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
    • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
    • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
    • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
    • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

    Java内存模型解决的问题

    当对象和变量被存放在计算机中各种不同的内存区域中时,就可能会出现一些具体的问题。Java内存模型建立所围绕的问题:在多线程并发过程中,如何处理多线程读同步问题与可见性(多线程缓存与指令重排序)、多线程写同步问题与原子性(多线程竞争race condition)。

1、多线程读同步与可见性

可见性(共享对象可见性):线程对共享变量修改的可见性。当一个线程修改了共享变量的值,其他线程能够立刻得知这个修改

线程缓存导致的可见性问题:

如果两个或者更多的线程在没有正确的使用volatile声明或者同步的情况下共享一个对象,一个线程更新这个共享对象可能对其它线程来说是不可见的:共享对象被初始化在主存中。跑在CPU上的一个线程将这个共享对象读到CPU缓存中,然后修改了这个对象。只要CPU缓存没有被刷新会主存,对象修改后的版本对跑在其它CPU上的线程都是不可见的。这种方式可能导致每个线程拥有这个共享对象的私有拷贝,每个拷贝停留在不同的CPU缓存中。

下图示意了这种情形。跑在左边CPU的线程拷贝这个共享对象到它的CPU缓存中,然后将count变量的值修改为2。这个修改对跑在右边CPU上的其它线程是不可见的,因为修改后的count的值还没有被刷新回主存中去。

解决这个内存可见性问题你可以使用:

  • Java中的volatile关键字:volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每个线程在每次使用volatile变量前都立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
  • Java中的synchronized关键字:同步快的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。
  • Java中的final关键字:final关键字的可见性是指,被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程就能看见final字段的值(无须同步)

重排序导致的可见性问题:

Java程序中天然的有序性可以总结为一句话:如果在本地线程内观察,所有操作都是有序的(“线程内表现为串行”(Within-Thread As-If-Serial Semantics));如果在一个线程中观察另一个线程,所有操作都是无序的(“指令重排序”现象和“线程工作内存与主内存同步延迟”现象)。

Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性:

  • volatile关键字本身就包含了禁止指令重排序的语义
  • synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入

指令序列的重排序:

1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作进行重排序:

数据依赖:

编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。(这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑)

指令重排序对内存可见性的影响:

当1和2之间没有数据依赖关系时,1和2之间就可能被重排序(3和4类似)。这样的结果就是:读线程B执行4时,不一定能看到写线程A在执行1时对共享变量的修改。

指令重排序改变多线程程序的执行结果例子:

flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入呢?

答案是:不一定能看到。

由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。

千里之行,始于足下。不积跬步,无以至千里

标签: [多线程]

以上是关于4.9--Java多线程之Java内存模型+volatile总结--(复习+总结篇)---一步一个脚印的主要内容,如果未能解决你的问题,请参考以下文章

多线程之内存模型

Java杂记之JVM内存模型

Java多线程之ThreadLocal和Volatite的使用

Java多线程之内存可见性

华为面试官问我关于“多线程之内存模型”,看完这篇,自信满满!

细说Java多线程之内存可见性:学习笔记