共计 4012 个字符,预计需要花费 11 分钟才能阅读完成。
前言
在并发编程中,当多个线程同时拜访同一个 共享的可变变量 时,会产生不确定的后果,所以要编写线程平安的代码,其本质上是对这些可变的共享变量的拜访操作进行治理。导致这种不确定后果的起因就是 可见性
、 有序性
和原子性
问题,Java
为解决可见性和有序性问题引入了 Java 内存模型,应用 互斥
计划(其外围实现技术是 锁
)来解决原子性问题。这篇先来看看解决可见性、有序性问题的 Java 内存模型(JMM)。
什么是 Java 内存模型
Java 内存模型在维基百科上的定义如下:
The Java memory model describes how threads in the Java programming language interact through memory. Together with the description of single-threaded execution of code, the memory model provides the semantics of the Java programming language.
内存模型限度的是共享变量,也就是存储在堆内存中的变量,在 Java 语言中,所有的实例变量、动态变量和数组元素都存储在堆内存之中。而办法参数、异样解决参数这些局部变量存储在办法栈帧之中,因而不会在线程之间共享,不会受到内存模型影响,也不存在内存可见性问题。
通常,在线程之间的通信形式有共享内存和消息传递两种,很显著,Java 采纳的是第一种即 共享的内存模型,在共享的内存模型里,多线程之间共享程序的公共状态,通过读 - 写内存的形式来进行隐式通信。
从形象的角度来看,JMM 其实是 定义了线程和主内存之间的关系
,首先,多个线程之间的共享变量存储在主内存之中,同时每个线程都有一个本人公有的本地内存,本地内存中存储着该线程读或写共享变量的正本(留神:本地内存是 JMM 定义的抽象概念,实际上并不存在)。形象模型如下图所示:
在这个形象的内存模型中,在两个线程之间的通信(共享变量状态变更)时,会进行如下两个步骤:
- 线程 A 把在本地内存更新后的共享变量正本的值,刷新到主内存中。
- 线程 B 在应用到该共享变量时,到主内存中去读取线程 A 更新后的共享变量的值,并更新线程 B 本地内存的值。
JMM 实质上是在硬件(处理器)内存模型之上又做了一层形象,使得利用开发人员只须要理解 JMM 就能够编写出正确的并发代码,而无需过多理解硬件层面的内存模型。
为什么须要 Java 内存模型
在日常的程序开发中,为一些共享变量赋值的场景会常常碰到,假如一个线程为整型共享变量 count
做赋值操作(count = 9527;
),此时就会有一个问题,其它读取该共享变量的线程在什么状况下获取到的变量值为 9527
呢?如果短少同步的话,会有很多因素导致其它读取该变量的线程无奈立刻甚至是永远都无奈看到该变量的最新值。
比方缓存就可能会扭转写入共享变量正本提交到主内存的秩序,保留在本地缓存的值,对于其它线程是不可见的;编译器为了优化性能,有时候会改变程序中语句执行的先后顺序,这些因素都有可能会导致其它线程无奈看到共享变量的最新值。
在文章结尾,提到了 JMM
次要是为了解决 可见性
和有序性
问题,那么首先就要先搞清楚,导致 可见性
和有序性
问题产生的实质起因是什么?当初的服务绝大部分都是运行在多核 CPU 的服务器上,每颗 CPU 都有本人的缓存,这时 CPU 缓存与内存的数据就会有一致性问题了,当一个线程对共享变量的批改,另外一个线程无奈立即看到。导致可见性问题的实质起因是 缓存。
有序性是指代码理论的执行程序和代码定义的程序统一,编译器为了优化性能,尽管会恪守 as-if-serial
语义(不管怎么重排序,在单线程下的执行后果不能扭转),不过有时候编译器及解释器的优化也可能引发一些问题。比方:双重查看来创立单实例对象。上面是应用双重查看来实现提早创立单例对象的代码:
/**
* @author mghio
* @since 2021-08-22
*/
public class DoubleCheckedInstance {
private static DoubleCheckedInstance instance;
public static DoubleCheckedInstance getInstance() {if (instance == null) {synchronized (DoubleCheckedInstance.class) {if (instance == null) {instance = new DoubleCheckedInstance();
}
}
}
return instance;
}
}
这里的 instance = new DoubleCheckedInstance();
,看起来 Java
代码只有一行,应该是无奈就行重排序的,实际上其编译后的理论指令是如下三步:
- 调配对象的内存空间
- 初始化对象
- 设置 instance 指向刚刚曾经调配的内存地址
下面的第 2 步和第 3 步如果扭转执行程序也不会扭转单线程的执行后果,也就是说可能会产生重排序,下图是一种多线程并发执行的场景:
此时线程 B 获取到的 instance
是没有初始化过的,如果此来拜访 instance
的成员变量就可能触发空指针异样。导致 有序性
问题的实质起因是编译器优化。那你可能会想既然缓存和编译器优化是导致可见性问题和有序性问题的起因,那间接禁用掉不就能够彻底解决这些问题了吗,然而如果这么做了的话,程序的性能可能就会受到比拟大的影响了。
其实能够换一种思路,能不能把这些禁用缓存和编译器优化的权力交给编码的工程师来解决,他们必定最分明什么时候须要禁用,这样就只须要提供按需禁用缓存和编译优化的办法即可,应用比拟灵便。因而 Java 内存模型
就诞生了,它标准了 JVM 如何提供按需禁用缓存和编译优化的办法,规定了 JVM 必须恪守一组最小的保障,这个最小保障规定了线程对共享变量的写入操作何时对其它线程可见。
程序一致性内存模型
程序一致性模型是一个理想化后的实践参考模型,处理器和编程语言的内存模型的设计都是参考的程序一致性模型实践。其有如下两大个性:
- 一个线程中的所有操作必须依照程序的程序来执行
- 所有的线程都只能看到一个繁多的执行操作程序,不论程序是否同步
在工程师视角下的程序一致性模型如下:
程序一致性模型有一个繁多的全局内存,这个全局内存能够通过左右摇摆的开关能够连贯到任意一个线程,每个线程都必须依照程序的程序来执行内存的读和写操作。该现实模型下,工作时刻都只能有一个线程能够连贯到内存,当多个线程并发执行时,就能够通过开关就能够把多个线程的读和写操作 串行化。
程序一致性模型中,所有操操作齐全依照程序串行执行,然而在 JMM 中就没有这个保障了,未同步的程序
在 JMM 中不仅程序的执行程序是无序的,而且因为本地内存的存在,所有线程看到的操作程序也可能会不统一,比方一个线程把写共享变量保留在本地内存中,在还没有刷新到主内存前,其它线程是不可见的,只有更新到主内存后,其它线程才有可能看到。
JMM 对在 正确同步的程序
做了程序一致性的保障,也就是程序的执行后果和该程序在程序一致性内存模型中的执行后果雷同。
Happens-Before 规定
Happens-Before
规定是 JMM 中的外围概念,Happens-Before
概念最开始在 这篇论文 提出,其在论文中应用 Happens-Before
来定义分布式系统之间的偏序关系。在 JSR-133 中应用 Happens-Before
来指定两个操作之间的执行程序。
JMM 正是通过这个规定来保障跨线程的内存可见性,Happens-Before
的含意是 后面一个对共享变量的操作后果对该变量的后续操作是可见的
,束缚了编译器的优化行为,尽管容许编译器优化,然而优化后的代码必须要满足 Happens-Before
规定,这个规定给工程师做了这个保障:同步的多线程程序是依照 Happens-Before
指定的程序来执行的。目标就是 为了在不改变程序(单线程或者正确同步的多线程程序)执行后果的前提下,尽最大可能的进步程序执行的效率
。
JSR-133
标准中定了如下 6 项 Happens-Before
规定:
- 程序程序规定:一个线程中的每个操作,
Happens-Before
该线程中的任意后续操作 - 监视器锁规定:对一个锁的解锁操作,
Happens-Before
于前面对这个锁的加锁操作 - volatile 规定 对一个
volatile
类型的变量的写操作,Happens-Before
与任意前面对这个volatile
变量的读操作 - 传递性规定:如果操作 A
Happens-Before
于操作 B,并且操作 BHappens-Before
于操作 C,则操作 AHappens-Before
于操作 C - start() 规定:如果一个线程 A 执行操作
threadB.start()
启动线程 B,那么线程 A 的start()
操作Happens-Before
于线程 B 的任意操作 - join() 规定:如果线程 A 执行操作
threadB.join()
并胜利返回,那么线程 B 中的任意操作Happens-Before
于线程 A 从threadB.join()
操作胜利返回
JMM 的一个根本准则是:只有不扭转单线程和正确同步的多线程的执行后果,编译器和处理器轻易怎么优化都能够,实际上对于利用开发人员对于两个操作是否真的被重排序并不关怀,真正关怀的是执行后果不能被批改。因而 Happens-Before
实质上和 sa-if-serial
的语义是统一的,只是 sa-if-serial
只是保障在单线程下的执行后果不被扭转。
总结
本文次要介绍了内存模型的相干基础知识和相干概念,JMM 屏蔽了不同处理器内存模型之间的差别,在不同的处理器平台上给利用开发人员形象出了对立的 Java 内存模型(JMM)
。常见的处理器内存模型比 JMM 的要弱,因而 JVM 会在生成字节码指令时在适当的地位插入内存屏障(内存屏障的类型会因处理器平台而有所不同)来限度局部重排序。