文章目录
Java内存模型(Java Memory Model,JMM)
Java内存模型(Java Memory Model,JMM)基本概念
Java
虚拟机(JVM
)中定义了Java
内存模型(JMM
)。Java
内存模型(JMM
)是一种规范,它规范了Java
虚拟机(JVM
)与计算机内存是如何协同工作的。它规定了一个线程如何和何时可以看到其它线程修改过的共享变量的值,以及在必须时如何同步的访问共享变量。
要弄懂Java
虚拟机,首先我们要明白两个概念,一个是堆(Heap
),一个是栈(Stack
)。
- 堆(Heap)
堆(Heap
)是一个运行时的数据区,堆是由垃圾回收来负责的。
堆的优势是它可以动态分配内存大小,生存期也不必要事先告知编译器,Java
的垃圾收集器(GC
)会及时收走这些不再使用的数据。
它的缺点:由于需要在运行时动态分配内存,因此它的存取速度相对慢一些。
- 栈(Stack)
栈(Stack
)的优势是存取速度比堆要快,仅次于计算机里面的寄存器。栈的数据它是可以共享的,但是它的缺点是栈中数据的大小与生存期必须是确定的,缺乏一些灵活性。栈中主要存放一些基本类型的变量,比如我们小写的int
、shot
、byte
、float
、double
、boolean
、char
和对象句柄
。
Java
内存模型要求调用栈和本地变量存放在线程栈(Stack
)中。对象存放在堆(Heap
)上。
JMM运行原理
一个本地变量,它可以是指向对象的引用,这种情况下引用这个的本地变量,它是存放在线程栈中,但是对象本身是存放在堆上的。一个对象它可能包含方法,这些方法可能包含本地变量。这些本地变量它仍然是存放在线程栈中,即使这些方法的对象存放在堆上。
一个对象的成员变量可能会随着这个对象自身存放在堆上,不管这个对象是原始类型还是引用类型。静态成员变量跟随着类的定义,一起存放在堆上。存在在堆上的对象可以被所持有对这个对象引用的线程访问。
当一个线程可以访问一个对象的时候,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,他们都会都访问这个对象的成员变量,但是每一个线程都拥有了这个对象的私有拷贝,这个特别重要。
如果两个线程,它们同时调用了同一个对象中的同一个方法,他们都会访问这个对象的成员变量。但是这两个对象,这两个线程,他们都拥有的是对这个对象的私有拷贝。
对象(Object
)与成员(全局)变量跟随着类的定义,存放在堆(Heap
)上,而方法与本地(局部)变量,存放在栈(Stack
)中。其中两个线程栈中的methodOne()
方法中都含有本地变量Local variable2
,该本地变量存放在栈中,而本地变量所指向的对象Object 3
却存放在堆上,而堆上Object 3
的成员变量跟随着类的定义也存放在堆上,该成员变量指向了Object 2
与Object 4
,因而我们可以通过Object 3
的成员变量进而访问到Object 2
与Object 4
,如上图所示的那种情况。
计算机内部硬件架构
计算机内部硬件的基本概念
- CPU
一个现代计算机通常由两个或者多个CPU
,其中一个CPU
上又含有多个核。从这一点我们可以看出,在一个有两个或者多个CPU
的现代计算机上,同时运行多个线程是非常有可能的,而且每个CPU
在某个时刻运行一个独立的线程是肯定没问题的。这意味着我们的Java
程序是多线程的,在我们的Java
程序中,多个线程是可能同时并发执行的。
- 寄存器
每个CPU
都包含一系列寄存器,它们是CPU
内存的基础,CPU
在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU
访问寄存器的速度远大于主存。
- 高速缓存(Cache)
由于计算机的存储设备与处理器的运算速度之间有着几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度都尽可能接近处理器运算速度的高级缓存来作为内存与处理器之间的缓冲。将运算需要使用到的数据复制到缓存中,让运算能快速的进行。当运算结束后,再从缓存同步到内存之中。这样处理器就无需等待缓慢的内存读写了。CPU
访问缓存层的速度快于访问主存的速度。但通常比访问内部寄存器的速度,还是要慢一点。每个CPU
可能有一个CPU
的缓存层,一个CPU
还有多层缓存,在某一时刻,一个或者多个缓存行可能被读到缓存,一个或多个缓存行可能再被刷新回主存,也就是说,同一时间点可能会有很多操作在这里。
- 内存
一个计算机还包含一个主存,所有的CPU
都可以访问主存,主存通常比CPU
中的缓存大得多。
计算机内部硬件的运作原理
通常情况下,当 一个CPU
需要读取主存的时候,它会将主存的部分读取到CPU
缓存中,它甚至可能会将缓存中的部分内容,读到它的内部寄存器里面,然后在寄存器中执行操作。当CPU
需要将结果回写到主存的时候,它会将内部寄存器的值刷新到缓存中,然后在某个时间点,将值刷新回主存。
比如说在上图中,在主存(Main Memory
)中含有对象obj
,而在obj
中又含有成员变量count = 1
,现在左右两个CPU
同时对其执行 + 1
操作。我们可以看到,在左边的CPU
中,其实是将主存(Main Memory
)中的obj.count = 1
读取到左边的高速缓存(CPU Cache Memory
)中,然后再将左边高速缓存(CPU Cache Memory
)中的obj.count = 1
从左边的高速缓存(CPU Cache Memory
)中读取到左边的寄存器(CPU Registers
)中。在这里,通过左边CPU
进行计算,完成+ 1
操作,此时左边寄存器(CPU Registers
)中obj.count = 2
,然后再将该信息由左边寄存器(CPU Registers
)写入到左边高速缓存(CPU Cache Memory
)中,此时,在左边高速缓存(CPU Cache Memory
)中obj.count = 2
,但是由于该数据还没有写入到主存(Main Memory
)中,因而主存中的数据仍然是obj.count = 1
。
而此时右边的CPU
也开始执行了+ 1
操作,此时右边的CPU
将主存中的数据 obj.count = 1
读取到右边的高速缓存(CPU Cache Memory
)中,而此时右边的高速缓存(CPU Cache Memory
)中的数据仍然是obj.count = 1
,如上图所示的那种情况。
而后右边的高速缓存(CPU Cache Memory
)将obj.count = 1
读取到右边的寄存器(CPU Registers
),在这里通过右边的CPU
计算完成+ 1
操作,此时在右边的寄存器(CPU Registers
)中,obj.count = 2
,而右边的寄存器(CPU Registers
)又将该数据写入到右边的高速缓存(CPU Cache Memory
)中。此时就出现了,左右两边,两个高速缓存(CPU Cache Memory
)中的obj.count = 2
的情况,如上图所示。而我们本来执行的是两次+ 1
操作,我们期望的结果是obj.count = 3
,但是此时由于并发,导致了数据计算出现了错误的情况,而要解决这个问题,我们得使用volatile
关键字。
Java内存模型(JMM)与计算机硬件架构之间的关联关系
Java虚拟机内存模型(JVM)与计算机硬件架构之间的对应关系
这里是内存模型和硬件架构的一些关联。
从图中,我们可以看到Java
内存模型和硬件之间其实是存在着一些差异的,硬件内存架构它没有区分线程栈和堆,对于硬件而言所有的线程栈和堆都分布在主内存里面。部分线程栈和堆可能有时候会出现在CPU
缓存中和CPU
内部的寄存器里面。
Java内存模型(JMM),线程与计算机硬件架构之间的运行原理
线程(Thread
)之间共享变量它存贮在主内存里面。每一个线程都有一个私有的本地内存,本地内存它是Java
内存模型(JMM
)的一个抽象概念,它并不是真实存在的,它涵盖了缓存,写缓存区,寄存器,以及其它的硬件和编译器的优化。在本地内存中,它存储了该线程以读或写共享变量的一个拷贝的副本。
比如图示中的线程1(Thread 1
)如果要使用主内存(Main Memory
)的一个变量,那么它先拷贝出来一个这个变量的副本,放在自己的本地内存里面,从更低的层次来说,主内存就是硬件的内存,是为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存,优先存储于寄存器和高速缓存中。Java
内存模型中的线程的工作内存,是CPU
的寄存器和高速缓存的一个抽象的描述。而JVM
的静态内存存储模型,也就是我们说的JVM
内存模型,它只是一种对内存的物理划分而已。他只局限在内存,而且只局限在JVM
的内存。
现在如果线程中通信,它要求必须要通过主内存(Main Memory
),如果线程1(Thread 1
)和线程2(Thread 2
)之间要通信的话,必须要经历下面两个步骤:
第一步是线程1(Thread 1
)要把本地的内存1中更新过的共享变量刷新到主内存(Main Memory
)里面去,线程2(Thread 2
)则到主内存(Main Memory
)中去读取线程1(Thread 1
)之前已经更新过的线程变量。
我们还是重复一下之前的计算,主内存(Main Memory
)中的变量的值为1,线程1(Thread 1
)和线程2(Thread 2
)同时开始执行,线程1(Thread 1
)从主内存中拿到的值是1,存到自己的本地内存1里面,然后执行+ 1
的操作。线程1(Thread 1
)计算完得到的结果为2,然后将它写入到主内存(Main Memory
),主内存(Main Memory
)中共享变量变成2。同时执行的线程2(Thread 2
),它从主内存中拿到的值也是1,存到线程2(Thread 2
)的本地内存2里面,然后执行+1操作,最后变成2。当线程1(Thread 1
)将2写回到主内存(Main Memory
)的同时,线程2(Thread 2
)也开始将自己计算后的结果写回到主内存(Main Memory
),而不是读取线程1(Thread 1
)写回到主内存(Main Memory
)中的2然后再重新计算。这两个线程在彼此计算的过程中,它们中的数据彼此是不可见的,因此计数就出现了错误。这个时候,我们就必须增加一些同步的手短来保证并发时程序处理的准确性。