文章目录
参考:参考内容
理解Java内存区域与Java内存模型
这里注意内存区域与内存模型的区别(
面试的时候别搞混了
)
Java内存区域
Java内存模型
Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图
需要注意的是,JMM与Java内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开的(稍后会分析)。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。关于JMM中的主内存和工作内存说明如下
硬件内存架构与Java内存模型
硬件内存架构
总而言之当一个CPU需要访问主存时,会先读取一部分主存数据到CPU缓存(当然如果CPU缓存中存在需要的数据就会直接从缓存获取),进而在读取CPU缓存到寄存器,当CPU需要写数据到主存时,同样会先刷新寄存器中的数据到CPU缓存,然后再把数据刷新到主内存中。
Java线程与硬件处理器
在Windows系统和Linux系统上,Java线程的实现是基于
一对一
的线程模型,所谓的一对一模型,实际上就是通过语言级别层面程序去间接调用系统内核的线程模型,即我们在使用Java线程时,Java
虚拟机内部是转而调用当前操作系统的内核线程来完成当前任务。这里需要了解一个属于,内核线程(Kernel-Level Thread,KLT),它是由操作系统内核(Kernel)支持的线程,内核通过操作调度器进而对线程执行调度,并将线程的任务映射到各个处理器上,
Java内存模型与硬件内存架构的关系
JMM
为什么引入JMM
为了解决类似上述的问题,
JVM
定义了一组规则,通过这组规则来决定一个线程对共享变量的写入何时对另一个线程可见,这组规则也称为Java内存模型(即JMM)
,JMM是围绕着程序执行的原子性
、有序性
、可见性
展开的,下面我们看看这三个特性。
原子性
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
比如对于一个静态变量int x,两条线程同时对他赋值,线程A赋值为1,而线程B赋值为2,不管线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操作是没有干扰的,这就是原子性操作,不可被中断的特点。
可见性
由工作内存和主内存同步延迟现象或者指令重拍导致可见性问题
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。
对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。但在多线程环境中可就不一定了,前面我们分析过,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
,另外指令重排以及编译器优化也可能导致可见性问题,通过前面的分析,我们知道无论是编译器优化还是处理器优化的重排现象,在多线程环境下,确实会导致程序轮序执行的问题,从而也就导致可见性问题。
有序性
有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,
后半句则指指令重排现象和工作内存与主内存同步延迟现象。
JVM中如何保证原子性可见性有序性
保证原子性
除了JVM自身提供的对基本数据类型读写操作的原子性外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性
- 可以使用
原子类:CAS
保证可见性
可以使用
synchronized
关键字或者volatile
关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见
保证有序性
以利用
volatile
关键字解决,因为volatile的另外一个作用就是禁止重排序优化(内存屏障)
happen-before的原则是什么
倘若在程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,在Java内存模型中,还提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下
定义了哪些指令不能重排
单线程的happen-before原则
:在同一个线程中,书写在前面的操作happen-bvefore后面的操作锁的happen-before原则
: 解锁必然发生在随后的枷锁(lock)之前、- 1voliatle规则1:volatile变量的写,先发生于读,这保证了volatile变量的可见性
传递性
:A先于B,B先于C,则A比先于C线程的start()方法先于它的每一个动作
线程的中断先于被中断线程的代码
线程的所有操作先于线程的终结
对象创建的happen-before原则:一个对象的初始化完成先于它的finalize方法调用
上述8条原则无需手动添加任何同步手段(synchronized|volatile)即可达到效果