Java并发编程(一) —Java内存模型

前言

Java虚拟机规范中定义了Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现Java程序在各平台都能达到一致的内存访问效果。在并发编程中,我们通常要处理两种问题,状态如何在多线程的操作下实现同步?线程之间如何通信?Java内存模型的规范就对线程之间的通信与同步提供了依据。

概述
物理机中处理并发

在物理机中的并发处理器要与各种组件交互,比如内存,磁盘等。由于处理器的处理速度远远大于存储设备的读写速度,所以在现代的计算机中加入了高速缓存来更加有效的利用处理器(比如:CPU有一级缓存、二级缓存、三级缓存等)。加入了高速缓存,就有了一个新的问题:缓存一致性。在多处理器中,每个处理器都有自己的高速缓存,同时又共享主内存。当多个处理器共同处理主内存中的相同数据时,他们的高速缓存就可能存在数据一致性的问题。为了解决一致性的问题,计算机就针对高速缓存设定了相应的读写协议一级置换算法等。
这里写图片描述

Java虚拟机处理并发

在Java程序中处理并发时,主要是处理多线程之间的并发过程。Java虚拟机规范定义了Java内存模型,一次为依据处理Java并发编程。Java内存模型规定了所有的变量都存储在主内存中,而每个线程又有自己的工作内存(就像物理机中处理器有自己的高速缓存一样),这样就存在了一致性问题。在Java中,线程之间的交互就通过Java内存模型控制,进而保证数据的一致性。
这里写图片描述

Java内存模型的原理

上面讲到的Java虚拟机通过Java内存模型(简称JMM)处理线程之间的通信与同步。可以看到,线程之间通过共享内存进行通信。JMM决定一个线程对共享变量的写入何时对另一个线程可见,即Java内存模型需要保证原子性、可见性、有序性。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

内存间的交互操作

Java内存模型定义了8中操作来完成主内存和工作内存之间的交互。

  • lock(锁定): 作用于主内存的变量,它把一个变量标识为一个线程独占的状态,相当于对该变量加锁。
  • unlock(解锁): 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定,相当于对该变量释放锁。
  • read(读取): 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load指令使用。
  • load(载入): 作用于工作内存的变量,它把被read操作取得的变量值放入到工作内存的变量副本中。
  • use(使用): 作用于工作内存的变量,当JVM执行程序时,需要用到该变量时,就会执行这个操作。
  • assign(赋值): 作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量。
  • store(存储): 作用于工作内存的变量,它把工作内存中的一个变量的值传递到主内存中,以便随后的write操作使用。
  • write(写入): 作用于主内存的变量,它把被store操作存储的工作内存中的变量值放入到主内存的变量中。
  • 在使用上述8中操作进行内存之间的交互时,Java内存模型也规定了操作规则:

    1. 不允许read和load、store和write操作之一单独出现,即不允许于一个变量从主内存读取但工作内存不接受,或者工作内存发起写入操作但主内存不接受得我情况发生。
    2. 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变后必须把该变化同步到主内存中。
    3. 不允许工作线程无原因的(没有assign赋值操作)把数据从线程的工作内存同步回主内存中。
    4. 不允许工作内存使用一个未被初始化的变量,也就是说对一个变量执行use之前,必须执行了assign和load操作。
    5. 一个变量在同一时刻只允许被同一个线程执行lock操作,但lock操作可以被同一个线程执行多次。同时,执行多次lock后,也需要执行相同次数的unlock操作才能解锁变量。
    6. 如果对一个变量执行lock操作,将会清空工作内存中词变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
    7. 如果一个变量没用被执行过lock操作,就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
    8. 对一个变量执行unlock操作之前,必须先把变量的值同步到主内存中(执行store、write操作)。

    这8中操作以及相关的规则确保了线程并发情况下,保证内存中变量的一致性。

    指令重排序

    在物理机中,处理器可能会对执行的代码进行乱序执行优化,处理器在计算之后将乱序结果重组,保证该结果与顺序执行的结果一致。同样的,Java虚拟机编译器中也有类似的指令重排序优化。重排序分为3种:

    • 编译器优化重排序。编译器在不改变单线程语义的前提下,可以重新安排语句的执行顺序。
    • 指令级并行的重排序。现代处理器采用了指令级并行处理技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变对应机器指令的执行顺序。
    • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行的。 重排序在程序执行时作为一种优化手段也需要遵循一些规则。
    数据依赖性

    如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性。 编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序。

    as-if-serial语义

    不管怎么重排序,单线程下的执行结果不能被改变,编译器、runtime和处理器都必须遵守as-if-serial语义。

    内存屏障(Memory Barrier)

    通过内存屏障可以禁止特定类型处理器的重排序,从而让程序按我们预想的流程去执行。内存屏障,又称内存栅栏,是一个CPU指令,基本上它是一条这样的指令:

    • 保证特定操作的执行顺序。
    • 影响某些数据(或则是某条指令的执行结果)的内存可见性。

    内存屏障类型表

    happens-before规则

    从字面上理解,就是一个操作先于另外一个操作。JMM规定,当一个操作的结果对另外一个操作可见时,着两个操作必须存在happens-before关系。

    • 规则一:一个线程中的每个操作,happens-before与该线程的任意后续操作。
    • 规则二:对于一个锁的解锁,happens-before于随后对这个锁的加锁。
    • 规则三:对一个volatile变量的写,happens-before与任意后续对这个volatile域的读。
    • 规则四:如果A happens-before B,B happens-before C,则A happens-before C,即传递性。

    happens-before是JMM中关键的概念,有了happens-before规则,便确定了操作之间的相互顺序,也就可以约束不同线程间操作的同步关系。

    顺序一致性内存模型

    顺序一致性内存模型是一个理论参考模型,我们可以这么理解,当程序正确同步时,程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同,也就是程序会按照我们设想中的顺序执行,就像不同的线程同步执行结果和串行执行的结果一样。

    这里先讲一个概念——数据竞争:在一个线程中写一个变量,在另外一个线程读同一个变量,而且写和读没有通过同步来排序,那么这两个线程是存在数据竞争的。所以当两个线程存在数据竞争时,如果未正确同步,就可能产生不一样的执行结果。

    在顺序一致性内存模型中,一个线程所有的操作必须按照程序的顺序来执行。所有的线程只能看到一个单一的操作执行顺序,也就是每个操作都必须原子执行且立刻对所有线程可见。顺序一致性模型只是一个参考,如果对程序不加控制,执行结果依然达不到预期。

    volatile内存语义

    关键字volatile时Java中提供的最轻量级的同步机制。但一个变量定义为volatile时,它就具备了两种特性,可见性和有序性。

    可见性就是当一个线程改变了这个变量的值时,新值对其他线程来说是可以立即得知的。值得注意的是,当声明为volatile变量的操作不具备原子性时,也就是变量的值与当前值相关时(比如 i ++),这个时候变量的操作是没有原子性的,并不能保证i的可见性。总的来说在这两种情况下,volatile变量才能保证可见性:

    1. 运算结果不依赖当前的值,或者能确保只有单一的线程修改变量的值。
    2. 变量不需要与其他的状态变量共同参与不变约束。

    有序性就是禁止指令重排序优化。

    对于volatile写来说,当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存;对于volatile的读来说,当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来从主内存中读取共享变量。

    final域的内存语义

    对于final语义的解释主要是在读final域之前,final与已经被初始化过了。
    实现final语义主要是定义final域的重排序规则,写final域的重排序规则:1.JMM机制编译器把final域的写重排序到构造函数外;2.编译器会在final域写之后,构造函数return之前,插入一个storestore屏障,这个屏障禁止处理器重排序把final域的写重排序到构造函数之外。

    锁的内存语义

    锁是Java中重要的同步机制,锁可以让临界区互斥执行,还可以让释放锁的线程向获取锁的线程发送消息。
    锁的内存语义就可以总结为,当线程释放锁时,JMM会把该线程中的共享变量刷新到主内存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效,被监视器保护的临界区代码必须从主内存中获取共享变量。

    总结

    以上就是Java内存模型的基本原理。Java内存模型是一个抽象的概念,它解决了Java中线程之间的通信与同步问题,对线程间的通信与同步制定了规范。比如:Java内存模型中的重排序、内存屏障、happens-before原则、顺序一致性、volatile语义、锁语义等。这些规范一同保证了线程间的通信与同步的正确运行。
    在进一步来说,Java内存模型是根据可见性、原子性以及有序性3个特性来建立的。
    原子性:由Java内存模型的读写操作以及锁机制保证。
    可见性:在单线程程序中不存在可见性问题。Java内存模型通过保证正确的多线程同步来实现可见性。
    有序性:在Java内存模型中有内存屏障、指令重排序优化保证程序的有序性。
    这三种特性都是用来保证多线程下程序能够正确的运行的基础。

    猜你喜欢

    转载自blog.csdn.net/programerxiaoer/article/details/80502634