-
JMM和物理机内存模型不是一个范畴。
-
JMM和Java运行时数据区没有直接对应关系。
03以史为鉴-回看计算机内存模型
========================
**1、**现代计算机内存模型
物理机遇到的并发问题与Java虚拟机中的情况有不少相似之处,物理机对并发问题的处理方案对虚拟机的实现也有相当大的参考价值。现代计算机中,CPU的指令速度远远超过内存的存取速度,由于计算机的存储设备与CPU的运算速度有几个数量级的差距,所以现在计算机中都不得不加入一层读写速度尽可能接近CPU运算速度的高速缓存(cache)来作为内存和CPU之间的缓冲。
基于高速缓存的存储交互很好的解决了CPU和内存的速度的矛盾,但也引入了一个新的问题,缓存一致性,在多处理器系统中,每个CPU都有自己的高速缓存,而他们又共享同一主内存,当多个处理器运算任务都涉及到同一块主内存区域时,将可能导致各自的缓存数据不一致。为了解决这个问题,需要各个处理器在访问内存时,需要遵循一些协议,例如MSI、EMSI、MOSI等。
图1 计算机内存模型
2、缓存一致性
为了解决这个问题,先后有过两种办法:
- 总线锁机制
总线锁就是使用CPU提供的一个LOCK#信号,当一个处理器在总线上输出此信号,其他处理器的请求将被阻塞,那么该处理器就可以独占共享锁。这样就保证了数据一致性。
- 缓存锁机制
但是总线锁定开销太大,我们需要控制锁的力度,所以又有了缓存锁,核心就是缓存一致性协议,不同的CPU硬件厂商实现方式稍有不同,有MSI、MESI、MOSI等。
3、多线程编程面临的问题
多线程编程面临的两个重要的问题是:
-
线程之间的通信
-
线程之间的同步
线程之间的通信是指线程之间通过什么方式来交换信息。
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
线程的通信方式:
-
共享内存
-
消息传递
在共享内存的并发模式里,线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来实现隐式通信。
在消息传递的并发模式里,同步是显式进行的,程序员必须显式指定某个方法或某段代码需要在线程之间互斥进行。
图2 共享内存并发模型
在消息传递的并发模式里,线程之间没有公共状态,线程之间必须明确发送消息来显式进行通信。
在消息传递的并发模型里,同步是隐式进行的,由于消息发送必然在消息接收之前,因此同步是隐式进行的。
图3 消息传递并发模型
04师夷长技-直面JSR133
=======================
1、JSR133是什么
JSR-133规范,即Java内存模型与线程规范,由JSR-133专家组开发。JSR-133规范是JSR-176(定义Java平台Tiger(5.0)发布版的重要特性)的一部分。本规范的标准内容将合并到Java语言规范、Java虚拟机规范以及java.lang包的类说明中。
2、 JSR133倾诉的对象是谁
身边好多同事反馈看不懂JSR133的内容,一方面是因为文档全部为英文,并且包含大量的专业英语。另外一方面是没有弄明白JSR133倾诉的对象到底是谁。如果弄明白的倾诉的对象,然后对号入座就能理解JSR133在说什么。JSR133倾诉的对象有两个,一个是使用者(程序员),另外一个是JMM的实现方(JVM)。面向程序员,JSR133通过happens-before规则给使用者提供了同步语义的保证。面向实现者,JSR133限制了编译器和处理器的优化,如下图4:
图4 JSR133整体视图
3、JSR133的主要内容是什么
JSR133主要描述了JMM的主要的规则和限制,并详细阐述了一些同步原语的内存语义,详细的请查看下一章节,JSR133的目录,如下图5:
图5 JSR133原文目录
05抽丝剥茧-专注JMM
================
1 JMM内存模型概述
前面在第三章节,讲述了共享内存和消息传递并发模型,java采用的是共享内存并发模型。
在java中,所有的实例域,静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。局部变量,方法参数和异常处理器参数不会在线程之间共享,他们不会有内存可见性问题,也不受内存模型的影响。
java线程之间的通信由java内存模型(JMM)控制,JMM决定了一个线程对共享变量的写入何时对另一个线程可见。JMM定义了多线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地化内存,本地内存中存储了该线程用以读/写共享变量的副本。本地内存只是JMM的抽象,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。java内存模型的抽象示意,如图6:
图6 JMM内存模型
2 重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。总的来说重排序分成两类:
**编译器优化的重排序。**编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
**处理器重排序。**现在处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
这些重排序可能会导致多线程出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。
JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
重排序对多线程的影响
下面我们从一个很经典的代码例子说明重排序的问题,代码如下:
class RecordExample {int a = 0 ;
boolean flag = false ;public void write(){
a = 1 ; //步骤1flage = true ; //步骤2
}public void reader(){
if(flag){ //步骤3int i = a * a; //步骤4
}``}
flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入呢?
答案是:不一定能看到。
由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。当操作1和操作2重排序时,可能产生什么效果?如下图7。
图7 程序执行时序图
如上图,操作1和操作2做了重排序。程序执行时,线程A首先写标记变量flag,随后线程B读取这个变量。由于条件判断为真,线程B将读取变量a。此时,变量a还没有被线程A写入,在这里多线程的语义被重排序破坏了!
3 原子性、可见性、有序性
原子性:
一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在java中当我们讨论一个操作具有原子性问题一般是指这个操作会被线程的随机调度打断。比如下面的操作:
int a = 1; //原子操作int a = b; //非原子操作,分两步操作第一步读取b的值,第二部将b赋值a
int a = a + 1; //非原子操作,分两步操作第一步读取a的值,第二部将计算结果赋值给a``a ++ ; //非原子操作,同上
JMM对原子性问题的保证如下:
**自带原子性保证:**在java中,对基本数据类型的变量的读取和赋值操作是原子性操作。
**synchronized:**synchronized可以保证边界操作结果的原子性。synchronized可以防止多个线程并发的执行同一段代码,从结果上保证原子性。
**Lock锁:**Lock锁保证原子性的原理和synchronized类似。
**原子类操作:**JDK提供了很多原子操作类来保证操作的原子性,例如基础类型:AtomicXxx;引用类型AtomicReference等。原子类的底层是使用CAS机制,这个机制对原子性的保证和synchroinized有本质的区别。CAS机制保证了整个赋值操作是原子的不能被打断,二synchronized只能保证代码最终执行结果的正确性,也就是说,synchronized消除了原子性问题对代码最后执行结果的影响。
可见性:
在多线程环境下,一个线程对共享变量的修改,不仅要对本线程可见,而且要对其他线程可见。造成可见性的主要原因是由于CPU多核心和高速缓存(L1,L2,L3)。JMM对可见性问题,提供了如下保证:
**volatile:**使用volatile关键字修饰一个变量可以保证变量的可见性,大概的保证语义如下(详细的参看volatile的内存语义章节)
-
线程对共享变量的副本做了修改,会立刻刷新最新值到主内存中。
-
线程对共享变量的副本做了修改,其他其他线程中对这个变量拷贝的副本会时效;其他线程如果需要对这个共享变量进行读写,必须重新从主内存中加载。
**synchronized:**使用synchronized代码块或者synchronized方法也可以保证共享变量的可见性。当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监听器保护的临界区代码必须从主内存中读取共享变量,从而实现共享变量的可见性。
**Lock锁:**使用Lock相关实现类也可以保证共享变量的可见性。其原理同synchronized。
**原子操作类:**原子类底层使用的是CAS机制。java中CAS机制每次都会从主内存中获取最新值进行compare,比较一致之后才会将新值set到主内存中去。而且这个操作是一个原子操作,所以CAS每次操作每次拿到的都是主内存中的最新值,每次set的值也会立即写到主内存中。
有序性:
程序执行的顺序按照代码的先后顺序执行。在JMM允许的重排序环境下,单线程的执行结果和没有重排序的情况下保持一致。JMM中提供一下方式来保证有序性:
**happens-before原则:**happens-before原则是java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,也就是说发生操作B之前,操作A产生的影响能被操作B观察到。这里的“影响”包括修改共享变量,方法调用。详细的happens-before说明请参看happens-before原则章节。
**synchronized机制:**synchronized能够保证有序性是因为synchronized可以保证同一时间只有一个线程访问代码块,而单线程环境下,JMM能够保证代码的串行语义;虽然使用synchronized的代码块,还可以发生指令重排序,但是synchronized可以保证只有一个线程执行,所以最后的结果还是正确的。
**volatile机制:**volatile的底层是使用内存屏障(详细请参看内存屏障章节)来保障有序性的。写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后。读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。
多线程面临的两个问题线程之间的通信和线程之间的同步,这两个问题如果仔细分析,从结果的角度看线程之间的通信就是可见性问题,线程之间的同步就是原子性和有序性的问题。
总结JMM对特性提供的支持如下:
特性 | volatile关键字 | synchronized关键字 | Lock接口 | Atomic变量 |
原子性 | 无法保障 | 可以保障 | 可以保障 | 可以保障 |
可见性 | 可以保障 | 可以保障 | 可以保障 | 可以保障 |
有序性 | 一定程度 | 可以保障 | 可以保障 | 无法保障 |
4 happens-before原则
JSR133使用happens-before来阐述操作之间的内存可见性。在JMM中,如果一个操作的结果需要对另一个操作可见,那么这两个操作之间必然要存在happens-before关系。这里提到的两个操作既可以是一个线程之内,也可以是不同线程之间。
在《并发编程的艺术》一书中,对happens-before的定义如下:
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是一个线程之内,也可以是不同线程之间。两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。
happens-before规则如下:
**程序顺序规则(Program Order Rule):**一个线程中的每个操作,happens-before于该线程中的任意后续操作。
**监视器锁规则(Monitor Lock Rule):**对一个锁的解锁,happens-before于随后对这个锁的加锁。
**volatile变量规则(Volatile Variable Rule):**对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
**start()规则(Thread Start Rule):**如果线程A执行线程B.start()(启动线程B),那么A线程的B.start()操作happens-before于线程B中的任意操作。
**join()规则(Thread Join Rule):**如果线程A执行线程B.join()并成功返回,那么线程B中的任意操作happens-before于线程A从B.join()操作成功返回。
**程序中断规则(Thread Interruption Rule):**对线程interrupt()的调用happens-before于被中断线程的interrupted()或者isInterrupted()。
**finalizer规则(Finalizer Rule):**一个对象构造函数的结束happens-before于该对象finalizer()的开始。
**传递性规则(Transitivity):**如果A happens-before B,且B happens-before C ,那么A happens-before C。
了解了happens-before原则,下面举例帮助理解:
private int value = 0;public void setValue(int value)
{
this.value = value;}
public int getValue(){` `return value;
}
假设两个线程A和B,线程A先(在时间上先)调用了这个对象的setValue(1),接着线程B调用了getValue()方法,那么B的返回值是多少?
对照happens-before原则,上面的操作不满下面的条件:
-
不是同一个线程,所以不涉及:程序顺序规则。
-
不涉及同步,所以不涉及:监视器锁规则。
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
[外链图片转存中…(img-kqDwFU3Q-1715723927522)]
[外链图片转存中…(img-N9yxgmUo-1715723927522)]
[外链图片转存中…(img-dlhsB0zL-1715723927522)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!