Java并发深度总结:JMM(Java内存模型)概述

怕什么真理无穷,进一寸有进一寸的欢喜。

1. 并发编程的两个问题

并发编程需要处理的两个关键问题是:

  • 线程之间如何通信(线程之间以何种机制来交换信息) 。
  • 线程之间如何同步(控制不同线程之间操作发生相对顺序)。

在命令式编程中,线程之间的通信机制有两种:

  • 共享内存:线程之间共享程序的公共状态,线程之间通过读/写内存中的公共状态来隐式进行通信。例如共享对象。该模型下,同步是显式的,也就是说程序员必须显式指定某个方法或某段代码需要在线程之间 互斥执行。
  • 消息传递:线程之间通过明确的发送消息来显式进行通信。如wait()和notify()。该模式下,同步是隐式进行的,因为消息的发送必须在消息的接收之前。

Java 的并发采用的是 共享内存模型,线程之间的通信对程序员完全透明。同步的底层使用的是临界区对象,是指当使用某个线程访问共享资源时,必须使代码段独享该资源,不允许其他线程访问该资源。

2. Java内存模型的抽象

2.1 线程安全的三要素
  • 原子性(Atomicity):表明一个操作或多个操作是不可分割,不可中断的,要么全部执行,要么全部不执行。
  • 可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
  • 有序性(Ordering):如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。

Tip:

有序性补充说明:

  • “在本线程内观察,所有的操作都是有序的。” 指的是 线程内表现为串行执行。
  • “在一个线程中观察另一个线程,所有的操作都是无序的。” 指的是 指令重排序现象 和 工作内存与主内存同步延迟现象。
2.2 计算机内存模型

为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。

共享内存系统中 多线程程序 内存访问存在的问题:

  • 缓存一致性问题(可见性问题)。
  • 处理器优化和指令重排问题(原子性和有序性问题)。
2.2.1 缓存一致性(可见性)

随着CPU技术的发展,CPU的执行速度比内存访问速度快很多,因此在CPU和内存之间就出现了缓存。

多核CPU,多线程。每个核都有自己的缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

需要注意的是:

缓存一致性问题其实就是指线程安全要素中的可见性问题。

2.2.2 处理器优化和指令重排(原子性和有序性)

为了使得处理器内部的运算单元能尽量被充分利用,编译器和处理器常常对指令做重排序。

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

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

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

在这里插入图片描述
因此任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。

需要注意的是:

处理器优化和指令重排问题就指的是线程安全元素中的原子性和有序性。

2.2.3 缓存一致性和指令重排的解决方案

从共享内存系统中 多线程程序 内存访问存在的两大问题的说明中,我们可以知道,计算机内存模型想要保证共享内存的正确性(可见性、有序性、原子性),就必须为缓存一致性和指令重排问题提供解决方案。

  • 计算机内存模型通过遵循缓存一致性协议解决缓存一致性问题。
    在这里插入图片描述
    为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等。

缓存一致性协议内涵:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态, 当处理器对这个数据进行修改操作的时候,会重新从系统内存中吧数据读到处理器缓存行里。

Tip:

由于缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值(也就是说,缓存一致性协议存在总线风暴问题);

缓存一致性问题除了遵循缓存一致性协议外,也可以通过在总线加LOCK#锁的方式解决,总线加锁使得其他CPU无法使用总线,因此效率低下。

  • 计算机内存模型通过限制处理器优化和使用内存屏障解决指令重排问题。
2.3 Java内存模型与计算机内存模型的关系

前文对计算机内存模型进行了详细的说明,Java内存模型(JMM)就是一种符合计算机内存模型规范的规范。 也就是说Java内存模型算是计算机内存模型的一种。

因此,Java内存模型指屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证正确性的(可见性、有序性、原子性)一套规范

在这里插入图片描述
以上这幅图表明了Java内存模型在线程与主存之间的抽象关系。也就是说,Java内存模型保证了各个线程和主内存之间,共享变量读写的正确性(可见性、有序性、原子性)。

需要注意的是:

计算机内存模型和Java内存模型都只是一套规范,因此都是抽象概念,并不真实存在。

3. Java内存模型

3.1 工作内存和主内存

工作内存(本地内存):用于存放共享变量副本,一般为CPU高速缓存或寄存器。

主内存:存放线程之间的共享变量。
在这里插入图片描述
线程、工作内存和主内存的关系 :

  • 每个线程都有一个独立的工作内存,用于存储线程私有的数据。
  • Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。
  • 线程对变量的操作(读取赋值等)必须在工作内存中进行。(线程安全问题的根本原因)。

也就是说,线程不能直接操作主内存中的变量,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存。并且不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

3.1.1 工作内存和主内存的交互

Java内存模型中定义了8种操作来完成交互,虚拟机实现时必须保证这8种操作都是原子的、不可分割的(对于long和double类型的变量来说,load、store、read跟write在某些32位虚拟机平台上允许例外)。

  1. lock(锁定):作用于主存的变量,把一个变量标识为一条线程独占状态,一个变量在同一时间只能一个线程锁定。
  2. unlock(解锁):作用于主存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取):作用于主存变量,把一个变量的值从主存传输到工作内存,以便随后的load操作使用。
  4. load(载入):作用于工作内存变量,把 read 操作从主内存中读取的变量的值放入工作内存的变量副本中(副本是相对于主内存的变量而言的)。
  5. use(使用):作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作。
  6. assign(赋值):作用于工作内存变量,它把一个从执行引擎接收到的值赋值给工作内存变量,每当引擎遇到一个给变量赋值的字节码指令的时候即执行此操作;
  7. store(存储):作用于工作内存变量,把工作内存中一个变量的值传送到主存,以便随后的write操作使用。
  8. write(写入):作用于主存变量,把store操作从工作内存中得到的变量的值放入主存的变量中。

在这里插入图片描述

3.1.2 工作内存和主内存交互的规则

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

  • 不允许read和load、store和write操作之一单独出现,也就是不允许从主内存读取了变量的值但是工作内存不接收的情况,或者不允许从工作内存将变量的值回写到主内存但是主内存不接收的情况。
  • 不允许一个线程丢弃最近的assign操作,也就是不允许线程在自己的工作线程中修改了变量的值却不同步/回写到主内存
  • 不允许一个线程回写没有修改的变量到主内存,也就是如果线程工作内存中变量没有发生过任何assign操作,是不允许将该变量的值回写到主内存
  • 变量只能在主内存中产生,不允许在工作内存中直接使用一个未被初始化的变量,也就是没有执行load或者assign操作。也就是说在执行use、store之前必须对相同的变量执行了load、assign操作
  • 一个变量在同一时刻只能被一个线程对其进行lock操作,也就是说一个线程一旦对一个变量加锁后,在该线程没有释放掉锁之前,其他线程是不能对其加锁的,但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同。
  • 对变量执行lock操作,就会清空工作空间该变量的值,执行引擎使用这个变量之前,需要重新load或者assign操作初始化变量的值
  • 不允许对没有lock的变量执行unlock操作,如果一个变量没有被lock操作,那也不能对其执行unlock操作,当然一个线程也不能对被其他线程lock的变量执行unlock操作
  • 对一个变量执行unlock之前,必须先把变量同步回主内存中,也就是执行store和write操作。
3.2 Java内存模型的实现

Java内存模型也始终围绕着共享内存访问的正确性(可见性、有序性、原子性)设计的。

3.2.1 可见性

Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。

  • volatile:volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,底层使用了内存屏障。而普通变量则不能保证新值能够立即同步会主内存中。
  • synchronized:synchronized的可见性是由“对一个变量执行lock(加锁)操作之前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中 重新获取最新的值,对一个变量执行unlock(解锁)操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则获得的。
  • final:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去,那在其他线程中就一定能看见final字段的值,且该值不可修改。
3.2.2 原子性

Java内存模型能够直接保证read、load、use、assign、store、write操作的原子性。

  • 更大范围的原子性控制可以使用synchronized关键字,该关键字底层通过lock/unlock实现。
  • 除long和double之外的基本类型的赋值操作都是原子性的。
  • 所有引用reference的赋值操作都是原子性的。
3.2.3 有序性

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

  • volatile:volatile关键字本身就包含了禁止指令重排序的语义(通过加入内存屏障指令)
  • synchronized:synchronized关键字保证同一时刻只允许一条线程操作。

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

happens-before原则(先行发生原则)

  1. 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
  2. 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
  3. volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
  4. 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
  5. 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  6. 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
  7. 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  8. 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

Tip:

as-if-serial与happens-before的关系

  • as-if-serial:保证了单线程下,程序的执行结果不被改变。
  • happens-before:保证了多线程下,正确同步的程序的执行结果不发生改变。

也就是说,只要符合as-if-serial 和 happens-before 规则,JMM就能保证单线程程序和正确同步的多线程程序的执行结果,不会因为编译器或处理器的优化而改变。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值