Java 多线程 - 4 - Java 内存模型

并发问题

并发编程中,通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题.

1. 原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行.

一个很经典的例子就是银行账户转账问题:
比如从账户 A 向账户 B 转 1000 元,那么必然包括 2 个操作:从账户 A 减去 1000 元,往账户 B 加上 1000 元.

试想一下,如果这 2 个操作不具备原子性,会造成什么样的后果.假如从账户 A 减去 1000 元之后,操作突然中止.然后又从 B 取出了 500 元,取出 500 元之后,再执行 往账户 B 加上 1000 元的操作.这样就会导致账户A虽然减去了 1000 元,但是账户 B 没有收到这个转过来的 1000 元.

2. 可见性

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

//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;

假若执行线程 1 的是 CPU1,执行线程 2 的是 CPU2 .由上面的分析可知,当线程 1 执行 i =10 这句时,会先把i的初始值加载到 CPU1 的高速缓存中,然后赋值为 10,那么在 CPU1 的高速缓存当中i的值变为 10 了 ,却没有立即写入到主存当中.

此时线程 2 执行 j = i,它会先去主存读取 i 的值并加载到 CPU2 的缓存当中,注意此时内存当中i的值还是 0 ,那么就会使得j的值为 0 ,而不是 10 .

这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值.

3. 有序性

有序性:即程序执行的顺序按照代码的先后顺序执行.

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

上面代码定义了一个 int 型变量,定义了一个 boolean 类型变量,然后分别对两个变量进行赋值操作.从代码顺序上看,语句 1 是在语句 2 前面的,那么 JVM 在真正执行这段代码的时候会保证语句 1 一定会在语句2 前面执行吗?不一定,这里可能会发生指令重排序(Instruction Reorder).

下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的.

比如上面的代码中,语句 1 和语句 2 谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句 2 先执行而语句 1 后执行.

内存模型

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性.只要有一个没有被保证,就有可能会导致程序运行不正确.

Java 语言的内存模型由一些规则组成,这些规则确定线程对内存的访问如何排序以及何时可以确保它们对线程是可见的.下面我们将分别介绍Java内存模型的重排序,内存可见性和 happens-before 关系.

重排序

内存模型描述了程序的可能行为.具体的编译器实现可以产生任意它喜欢的代码 – 只要所有执行这些代码产生的结果,能够和内存模型预测的结果保持一致.这为编译器实现者提供了很大的自由,包括操作的重排序.

编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本.重排序后的指令,对于优化执行以及成熟的全局寄存器分配算法的使用,都是大有脾益的,它使得程序在计算性能上有了很大的提升.

重排序类型包括:

  • 编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本.
  • 处理器可以乱序或者并行的执行指令.
  • 缓存会改变写入提交到主内存的变量的次序.
内存可见性

由于现代可共享内存的多处理器架构可能导致一个线程无法马上(甚至永远)看到另一个线程操作产生的结果.所以 Java 内存模型规定了 JVM 的一种最小保证:什么时候写入一个变量对其他线程可见.

在现代可共享内存的多处理器体系结构中每个处理器都有自己的缓存,并周期性的与主内存协调一致.假设线程 A 写入一个变量值 V,随后另一个线程 B 读取变量 V 的值,在下列情况下,线程 B 读取的值可能不是线程 A 写入的最新值:

  • 执行线程 A 的处理器把变量 V 缓存到寄存器中.
  • 执行线程 A 的处理器把变量 V 缓存到自己的缓存中,但还没有同步刷新到主内存中去.
  • 执行线程 B 的处理器的缓存中有变量 V 的旧值.
Happens-before 关系

happens-before 关系保证:如果线程 A 与线程 B 满足 happens-before 关系,则线程 A 执行动作的结果对于线程 B 是可见的.如果两个操作未按 happens-before 排序,JVM 将可以对他们任意重排序.
JMM动作(Java Memeory Model Action),Java存储模型动作.一个动作(Action)包括:变量的读写、监视器加锁和释放锁、线程的start()和join().

happens-before 完整规则
(1)同一个线程中的每个 Action 都 happens-before 于出现在其后的任何一个 Action.

(2)对一个监视器的解锁 happens-before 于每一个后续对同一个监视器的加锁.

(3)对 volatile 字段的写入操作 happens-before 于每一个后续的同一个字段的读操作.

(4)Thread.start() 的调用会 happens-before 于启动线程里面的动作.

(5)Thread 中的所有动作都 happens-before 于其他线程检查到此线程结束或者 Thread.join()中返回或者 Thread.isAlive()==false.

(6)一个线程 A 调用另一个另一个线程 B 的 interrupt()都 happens-before 于线程 A 发现 B 被 A 中断( B 抛出异常或者 A 检测到 B 的 isInterrupted()或者 interrupted()).

(7)一个对象构造函数的结束 happens-before 与该对象的 finalizer 的开始

(8)如果 A 动作 happens-before 于 B 动作,而 B 动作 happens-before 与 C 动作,那么 A 动作 happens-before 于 C 动作.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

老高的IT职业路

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

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

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

打赏作者

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

抵扣说明:

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

余额充值