乐趣区

关于java:浅谈JMM和并发三大特性

Java 内存模型

这里首先理解一下计算机存储构造,如下图:

因为 CPU 和物理主存速度不统一问题,为了解决 CPU 读取内存指令和数据效率问题,诞生了 CPU 高速缓存。
同时 CPU 的运行并不是间接操作内存而是先吧内存里边的数据读到缓存,而内存的读和写操作的时候会造成不统一的问题呈现。

这时在 JVM 标准中定义一种 Java 内存模型 Java Memory Model,简称 JMM 来屏蔽掉各种硬件和操作系统的内存拜访差别以实现让 Java 程序在各种平台下能达到统一的内存拜访成果。
所以 Java 内存模型自身是一种形象的概念理论并不实在存在, 它形容的是一组规定或标准通过标准定制了程序中各个变量的拜访形式。

在 Java 并发环境下呈现线程平安的问题个别是因为主内存和工作内存数据不一致性和重排序导致的,而解决线程平安的问题最重要的就是了解这两种问题是怎么来的,那么,了解它们的外围在于了解 java 内存模型
在多线程条件下,多个线程必定会相互协作实现一件事件,一般来说就会波及到多个线程间互相通信告知彼此的状态以及以后的执行后果等,另外,为了性能优化,还会波及到编译器指令重排序和处理器指令重排序。

所以在并发编程中次要须要解决两个问题:
●线程之间如何通信
●线程之间如何实现数据同步
通信是指线程之间以何种机制来替换信息,次要有两种:共享内存和消息传递。Java 内存模型是共享内存的并发模型,线程之间次要通过读 - 写共享变量来实现隐式通信。
那么在 Java 程序中那些是共享变量,实例域,动态域和数组元素都是放在堆内存中(所有线程均可拜访到,是能够共享的)

JVMM 标准下,三大个性

上面咱们围绕多线程的可见性、原子性、和有序性

可见性

是指当一个线程批改了某一个共享变量的值, 其余线程是否可能立刻晓得该变更,JVMM 规定了所有的变量都存储在主内存中。
例如有 A、B 两个线程同时去操作主物理内存的共享数据 number=0,A 抢到 CPU 执行权, 将 number 刷新到本人的工作内存, 这个时候进行 number++ 的操作, 这个时候 number=1, 将 A 中的工作内存中的数据刷新到主物理内存, 这个时候, 马上告诉 B,B 从新拿到最新值 number= 1 刷新 B 的工作内存中。

Java 中一般的共享变量不保障可见性, 因为数据批改被写入内存的机会是不确定的, 多线程并发很可能呈现 ” 脏读 ”, 所以每个线程都有本人的工作内存, 线程本人的工作内存中保留了该线程应用到的变量的主内存正本拷贝, 线程对变量的所有操作 (读取、赋值等) 都必须在线程本人的工作内存中进行, 而不能间接读写主内存中的变量。不同线程之间也无奈间接拜访对工作内存中的变量, 线程间变量值的传递均须要通过主内存来实现。那么在 Java 中是通过 volatile 能够解决可见性问题的。

原子性

一个操作不能被打断,要么全副执行结束,要么不执行的。在这点上有点相似于事物操作,要么全副执行胜利,要么回退到该操作之前的状态。

有序性

在执行程序时,为了进步性能,编译器和处理器经常会对指令进行重排序。个别重排序能够分为如下三种:

●编译器优化的重排序。编译器在不扭转单线程程序语义的前提下,能够重新安排语句的执行程序。
●指令级并行的重排序。古代处理器采纳了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器能够扭转语句对应机器指令的执行程序。
●内存零碎的重排序。因为处理器应用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

如图,1 属于编译器重排序,而 2 和 3 统称为处理器重排序。这些重排序会导致线程平安的问题。针对编译器重排序,JMM 的编译器重排序规定会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些非凡的处理器重排序。
那么什么状况下,不能进行重排序了?上面就来说说数据依赖性。有如下代码:

double a =3.14 //A
double b = 1.0 //B
double area = a * b * b //C

这是一个计算圆面积的代码,因为 A,B 之间没有任何关系,对最终后果也不会存在关系,它们之间执行程序能够重排序。因而能够执行程序能够是 A ->B->C 或者 B ->A->C 执行最终后果都是 3.14,即 A 和 B 之间没有数据依赖性。

这是一个计算圆面积的代码,因为 A,B 之间没有任何关系,对最终后果也不会存在关系,它们之间执行程序能够重排序。因而能够执行程序能够是 A ->B->C 或者 B ->A->C 执行最终后果都是 3.14,即 A 和 B 之间没有数据依赖性。
具体的定义为:如果两个操作拜访同一个变量,且这两个操作有一个为写操作,此时这两个操作就存在数据依赖性这里就存在三种状况:1. 读后写;2. 写后写;3. 写后读,这三种操作都是存在数据依赖性的,如果重排序会对最终执行后果会存在影响。编译器和处理器在重排序时,会恪守数据依赖性,编译器和处理器不会扭转存在数据依赖性关系的两个操作的执行程序。
另外,还有一个比拟有意思的就是 as-if-serial 语义。

as-if-serial

as-if-serial 语义的意思是:不管怎么重排序(编译器和处理器为了提供并行度),(单线程)程序的执行后果不能被扭转。编译器,runtime 和处理器都必须恪守 as-if-serial 语义。as-if-serial 语义把单线程程序爱护了起来,恪守 as-if-serial 语义的编译器,runtime 和处理器独特为编写单线程程序的程序员创立了一个幻觉:单线程程序是按程序的程序来执行的。比方下面计算圆面积的代码,在单线程中,会让人感觉代码是一行一行程序执行上,实际上 A,B 两行不存在数据依赖性可能会进行重排序,即 A,B 不是程序执行的。as-if-serial 语义使程序员不用放心单线程中重排序的问题烦扰他们,也无需放心内存可见性问题。

happens-before 准则
具体的含意为:
1. 如果一个操作 happens-before 另一个操作,那么第一个操作的执行后果将对第二个操作可见,而且第一个操作的执行程序排在第二个操作之前。

这一点是 JMM 对程序员的承诺。从程序员的角度来说,能够这样了解 happens-before 关系:如果 A happens-before B,那么 Java 内存模型将向程序员保障——A 操作的后果将对 B 可见,且 A 的执行程序排在 B 之前。留神,这只是 Java 内存模型向程序员做出的保障!
2. 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要依照 happens-before 关系指定的程序来执行。如果重排序之后的执行后果,与按 happens-before 关系来执行的后果统一,那么这种重排序并不非法(也就是说,JMM 容许这种重排序)。

这一点是 JMM 对编译器和处理器重排序的束缚准则。正如后面所言,JMM 其实是在遵循一个根本准则:只有不改变程序的执行后果,编译器和处理器怎么优化都行。JMM 这么做的起因是:程序员对于这两个操作是否真的被重排序并不关怀,程序员关怀的是程序执行时的语义不能被扭转(即执行后果不能被扭转)。因而,happens-before 关系实质上和 as-if-serial 语义是一回事。

as-if-serial 和 happens-before 的比拟
●as-if-serial 语义保障单线程内程序的执行后果不被扭转,happens-before 关系保障正确同步的多线程程序的执行后果不被扭转。
●as-if-serial 语义给编写单线程程序的程序员发明了一个幻境:单线程程序是按程序的程序来执行的。happens-before 关系给编写正确同步的多线程程序的程序员发明了一个幻境:正确同步的多线程程序是按 happens-before 指定的程序来执行的。
●as-if-serial 语义和 happens-before 这么做的目标,都是为了在不扭转程序执行后果的前提下,尽可能地进步程序执行的并行度。

文章链接:浅谈 JMM 和并发三大个性

👉 如果本文对你有帮忙的话,欢送点赞|关注,非常感谢

退出移动版