之前虽然一直用java的线程,但是还是没有把线程的内存模型搞清楚,遂整理一下
Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
- 线程的通信
通俗讲就是线程交换信息。在命令式编程中,线程的通信机制有两种 : 共享内存 + 消息传递
1 在共享内存的并发模型中,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式通信。 其中典型的共享内存通信方式就是通过共享对象进行通信,常用的如 volatile 关键字。
2 在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显示进行通信,在java中典型的消息传递方式是wait()和notify()。
- 线程同步的机制
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。(典型的如信号量机制)
- Java的并发采用的是共享内存模型
Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。
- java内存模型
image.png上面讲到了Java线程之间的通信采用的是过共享内存模型,这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
说明,如果此时线程A要和线程B进行通信,就必须通过将本地共享变量的修改刷新到主内存中,然后由另外一个线程读取主内存中共享变量的值。
JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
-
JVM对Java内存模型的实现
image.png
JMM将内存分为 线程栈和堆区
JVM中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈。随着代码的不断执行,调用栈会不断变化。
线程栈还包含了当前方法的所有本地变量信息。一个线程只能读取自己的线程栈,也就是说,线程中的本地变量对其它线程是不可见的。即使两个线程执行的是同一段代码,它们也会各自在自己的线程栈中创建本地变量,因此,每个线程中的本地变量都会有自己的版本。所有原始类型(boolean,byte,short,char,int,long,float,double)的本地变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。对于原始类型的本地变量,一个线程可以传递一个副本给另一个线程,但它们之间是无法共享的。
image.png
堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量还是方法中的本地变量,它都会被存储在堆区。
一个本地变量如果是原始类型,那么它会被完全存储到栈区。
image.png
一个本地变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。
对于一个对象的成员方法,这些方法中包含本地变量,仍需要存储在栈区,即使它们所属的对象在堆区。
对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。
Static类型的变量以及类本身相关信息都会随着类本身存储在堆区。
堆中的对象可以被多线程共享。如果一个线程获得一个对象的引用,它便可访问这个对象的成员变量。如果两个线程同时调用了同一个对象的同一个方法,那么这两个线程便可同时访问这个对象的成员变量,但是对于本地变量,每个线程都会拷贝一份到自己的线程栈中。
-
硬件内存架构
image.png
image.png
当对象和变量存储到计算机的各个内存区域时,必然会面临一些问题,其中最主要的两个问题是:
- 共享对象对各个线程的可见性
- 共享对象的竞争现象
共享对象的可见性
image.png
当多个线程同时操作同一个共享对象时,如果没有合理的使用volatile和synchronization关键字,一个线程对共享对象的更新有可能导致其它线程不可见。
下图展示了上面描述的过程。左边CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。但这个变更对运行在右边CPU中的线程不可见,因为这个更改还没有flush到主存中:
为解决共享对象可见性这个问题,我们可以使用volatile关键字。volatile关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存。
竞争现象
如图所示,两个线程同时对主存中一个变量进行修改(+1操作), 但是又于双方拿到的数据都是对方未回写之前的,因此存在典型的CAS问题。
image.png
解决方法: sychronized 代码块
1 synchronized代码块可以保证同一个时刻只能有一个线程进入代码竞争区。(互斥性)
2 synchronized代码块也能保证代码块中所有变量都将会从主存中读,当线程退出代码块时,对所有变量的更新将会flush到主存,不管这些变量是不是volatile类型的。(保证代码块中变量同步主存)
- 支撑Java内存模型的基础原理
内存屏障
上面java内存模型中讲到的volatile是基于Memory Barrier实现的。如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。这意味着,如果写入一个volatile变量,就可以保证:
1 一个线程写入变量a后,任何线程访问该变量都会拿到最新值。
2 在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。
参考:
1 全面理解Java内存模型
2 volatile和synchronized的区别
网友评论