JAVA并发可见性、有序性、原子性与JMM内存模型详解

目录

1. JAVA并发三大特性

1.1 原子性

1.1.1 如何保证原子性

1.2 可见性

1.2.1 如何保证可见性

1.3 有序性

1.3.1 如何保证有序性

2. JAVA内存模型

2.1 JAVA内存模型的抽象结构

2.2 JAVA内存模型中主内存与本地内存的交互协议

2.2.1 八种原子操作解释

2.2.2 八种原子操作的规则

2.2.3 通过案例分析深入理解JAVA内存模型中主内存和工作内存的交互

2.3 内存含义

2.3.1 锁

2.3.2 volatile


1. JAVA并发三大特性

        java并发编程有三大特性,原子性、可见性和有序性,这三个特性也往往是java并发编程bug的源头。

1.1 原子性

  • 一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。
  •  在 Java中,对基本数据类型的变量的读取和赋值操作是原子性操作64位处理器)。
  • 不采取任何的原子性保障措施的自增操作并不是原子性的,比如i++操作。

注意: 在 32 位的机器上对 long 型变量进行加减操作是否存在并发隐患?

由java文档中可知,在32位机器上,long 和double 存在并发隐患,需要用volatile来保证。

        Chapter 17. Threads and Locks

1.1.1 如何保证原子性

  1. 通过 synchronized 关键字保证原子性
  2. 通过 Lock锁保证原子性
  3. 通过 CAS保证原子性

1.2 可见性

        可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

1.2.1 如何保证可见性

  1. 通过 volatile 关键字保证可见性
  2. 通过 内存屏障保证可见性
  3. 通过 synchronized 关键字保证可见性
  4. 通过 Lock锁保证可见性

1.3 有序性

        程序执行的顺序按照代码的先后顺序执行。 为了提升性能,编译器和处理器常常会对指令做重排。

1.3.1 如何保证有序性

  • 通过 volatile 关键字保证有序性
  • 通过 内存屏障保证有序性
  • 通过 synchronized关键字保证有序性
  • 通过Lock锁保证有序性

2. JAVA内存模型

在并发编程中,需要处理的两个关键问题

多线程之间如何通信: (线程之间以何种机制来交换数据)。

多线程之间如何同步 (控制不同线程间操作发生的相对顺序)。

线程之间常用的通信机制有两种:共享内存和消息传递,Java采用的是共享内存模型。

2.1 JAVA内存模型的抽象结构

        Java线程之间的通信由Java内存模型(Java Memory Model,简称JMM)控制,JMM决定一个 线程对共享变量的写入何时对另一个线程可见。

        从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了共享变量的副本。本地内存是JMM的一 个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

根据JMM的规定, 线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内
存中读取
由上图可知,如果线程A和线程B之间要通信的话,必须经历以下两个步骤:
  1. 线程A把本地内存A中更新过的共享变量刷新到主内存中 。
  2. 线程B到主内存中去读取线程A之前已更新过的共享变量。

所以,线程A无法直接访问线程B的工作内存,线程间通信必须经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序提供内存可见性的保证。

2.2 JAVA内存模型中主内存与本地内存的交互协议

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

2.2.1 八种原子操作解释

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

2.2.2 八种原子操作的规则

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

2.2.3 通过案例分析深入理解JAVA内存模型中主内存和工作内存的交互

        下图是线程A和线程B共享变量flag的案例。

 

内存屏障 (synchronized Threed.sleep(10) volatile)
cup上下文切换 (Threed.yield() Threed.sleep(0) )

2.3 内存含义

2.3.1 锁

锁获取和释放的内存语义:
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
synchronized关键字的作用是确保多个线程访问共享资源时的互斥性和可见性。在获取锁之前,线程会将共享变量的最新值从主内存中读取到线程本地的缓存中,释放锁时会将修改后的共享变量的值刷新到主内存中,以保证可见性。

2.3.2 volatile

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瑜伽娃娃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值