在java语言中,定义java内存模型(JMM)使得程序员更好的理解java并发,同时也屏蔽了java语言在不同处理器上的内存读取的不一致性。为了要达到这个要求,所有的芯片设计商需要满足相关的协议,用来保证数据的一致性、可靠性。Java语言的数据在jvm中分为堆、栈、计数器、本地方法栈、代码区。本次要讨论的JMM主要定义的是堆中的数据。而像栈、计数器、本地方法栈,代码区,这些区域的数据在单线程和多线程没有什么差别。只有在堆中的数据才有可能会受到多线程的影响,可能会产生数据不一致性。
并发编程中,有两个基本的问题是需要考虑解决的:多线程的通信和数据如何数据同步,Java语言是通过共享内存来解决这两个基本问题的,而共享内存的方式实际上也是最快的多线程的通信方式。如果使用共享内存的方式就必然面临一个问题:一个堆内变量被一个线程修改后何时可以被另一个线程发现,也就是多线程下的可见性问题怎么处理?Java语言正是通过这个内存模型(JMM)的定义来定义可见性问题。
在java内存模型中,定义了每个线程和主存之间的关系:一个共享变量在主存中存在一份,每个线程的本地存储变量都有一份副本,每个线程在读写的时候使用的是本地副本。值得注意的是:本地存储变量副本是一个抽象概念,实际中并不存在,只是JMM为了大家理解而定义的。所以真正的java内存模型的抽象示意图如下:
上面这个java内存模型是怎么定义数据同步和线程通信的呢?
数据通信和同步的过程:假设线程A修改了本地数据,那么线程B是怎么知道线程A已经修改了数据,并且修改后的数据是怎么样的?设想一下,如果线程A修改了数据,线程B并不知道,此时线程读取和写入的数据还是之前的数据,那就导致数据的同步出现了问题。根据上面的模型,我们可以知道如果线程B要知道线程A中的数据修改,那么必须有以下几个步骤:
- 线程A修改完本地的数据之后,同时将数据刷新到主存中
- 主存的数据发生改变时,线程B中的本地变量的副本地址不可用,导致线程B必须从主存中从新取数据
- 线程B从主存中取到数据之后,将 数据刷回本地副本中
经过上面几个步骤,线程B就会获取到线程A 的数据并且把最新的数据带回到本地副本中。
看完上面的流程,不知道你有没有想过这样一种场景:如果线程A在修改本地的时候,线程B也在修改本地的数据,同时线程A和线程B都准备把数据刷回 到主存中,这个时候当然会发生碰撞,那么cpu是怎么解决的呢 ?
这就涉及到了工作内存和主存的交互了,在Java中,jvm定义了以下8中操作用来完成线程中的内存和主存之间的交互,其中这些操作都是原子性的,这些操作分别是:
Lock:主存锁定,表示一个时间点只有一个线程能够锁住主存
Unlock:主存解锁,一旦解锁之后,其他线程就可以操作主存中的内存
Read:用于主存中将主存的值传递到本地副本,结合后面的load指令一起
Load:用于本地副本,表示主存的值通过read之后传递过来,在通过load指令载入到本地副本中
Use:用于本地副本,将工作内存中的值传给执行引擎,如果遇到使用变量的值就会有这个指令
Assign:用于本地副本,表示将执行引擎的值 传递给执行副本,如果遇到变量赋值就会有这个指令
Store:用于本地副本,表示将本地副本的值传递给主存,结合后面的write指令一起使用
Write:用于主存中将本地副本的值通过store传入后,存入主存中。
所以上面两个操作,线程A要将数据刷回主存需要先后执行store和write指令,线程B 要将数据从主存中刷回本地副本,需要先后执行read和load指令。当然执行上面的指令还有一些其他的规定:
- read和load、store和write操作之一不能单独出现;这个比较好理解,就是写入和读取不能一边写入,一边没接收
- assign操作不能被自己丢弃;也就是工作线程修改的值一定要刷回主存
- 没修过的变量不允许写会到主存;
- 变量只能在主存中产生;不允许本地副本中有一个未初始化的变量;
- 一个变量在某一时间只能有一个线程lock;
- unlock要结合unlock,不能单独执行unlock;
- unlock一个变量之前需要将数据同步到主存中;
最后回到上面的问题,如果A和B都要刷回数据,那实际上却是不知道最后的数据是A的还是B的 ,因为A和B都是经过store和write指令,而且两个指令都是原子性的,所以先后的顺序就决定了主存中最后的值了。
想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈