为什么要设计JAVA内存模型?
小陈:老王,看了上一篇的《CPU多级缓存模型》,有个疑问为什么还要有JAVA内存模型啊?
老王:这么来说吧,CPU多级缓存模型,只是一个规范,但是底层基于这个规范的实现还是有很多种的。比如:
(1)基于这个模型之上不同的计算机厂商可能对这个模型底层的实现不一样,包括底层运行的指令集、有的还基于多级缓存模型之上还引入了写换从器、无效队列等。
(2)不同的操作系统windows、linux的指令集不一样。
老王:
所以JAVA语言为了应对上述的问题:不同的系统、不同的计算机厂商的底层实现不同。基于它们之上设计了一个JAVA内存模型(JMM)的规范,就是为了屏蔽底层操作系统、厂商之间的差异性。程序员运行是只需要关注JMM,而不需关注底层系统、厂商指令的差异性,这些由JMM去适配不同的系统和厂商的指令集,这样就能实现一次编写,“到处乱跑”的跨平台效果。
小陈:哦哦,原来是为了屏蔽操作系统、不同的计算机厂商的底层的差异性,实现跨平台的无差异性运行的效果啊。
JAVA内存模型是怎么样的?
小陈:原因我大概知道了,那JAVA内存模型大概是个什么样的结构啊?
老王:给你看下下面这张图,你大概就理解了
老王:上图就是JAVA内存模型的大致结构图,JAVA内存模型定义了一个规范。那就是每个线程都有自己的工作内存,线程操作共享变量的时候需要从主内存读取到自己的工作内存,然后在传递给工作线程使用,共享变量修改后先刷新到工作内存,然后再刷新回主内存;这个JAVA内存模型是基于我们上一讲CPU多级缓存模型上建立的。
JAVA内存模型定义的八种原子操作
小陈:看了上面的图,JAVA内存模型的结构我基本知道了,但是JMM这哥们怎么工作的我还是很模糊啊?
老王:没关系,我来给你讲讲,JMM定义了8种指令,它工作的时候就是通过这8种指令来操作内存的。
包括怎么锁定变量、怎么将数据从主存传到工作内存、怎么传给正在被CPU调度的线程、修改之后CPU怎么传回工作内存、工作内存又怎么传递回主存:
- lock(锁定):把主内存中的一个共享变量标记为一个线程独享的状态
- unlock(解锁):把主内存的变量从线程独享的lock状态中解除出来
- read(读取):把主内存的一个共享变量传输工作内存中
- load(载入):把从主内存传输到工作内存的共享变量,赋值给工作内存中的变量副本
- use(使用):把工作内存中的变量副本的值,传递给执行引擎CPU
- assign(赋值):执行引擎(CPU)执行完之后,把修改过的变量值重新赋值给工作内存中的变量副本
- store(存储):把工作内存中修改过共享变量的值传递到主内存中
- write(写入):把传递到主内存中的变量值,重新写回给主内存的共享变量
小陈:...., 这都啥啊...
老王:给你画张图吧,你就了解了:
比如线程A要执行一个x++的操作,可能要经历下面的过程:
小陈:牛逼啊老王....
小陈:额,感觉不对啊,上面只有六种指令,还有另外两种lock、unlock指令是咋用的?
老王:这还不简单,再给你画张图就KO了。
比如还是上面的x++操作
(1)工作线程A操作该共享内存变量的时候,执行lock指令,主内存中的这个共享变量;同时告诉线程B这个共享变量我准备修改了,让它失效掉。
(2)B线程的变量副本失效之后,运行时候用到,需要到主内存重新读取(执行read、load操作放入工作内存);发现该主内存的变量被锁定了,读取失败;此时相当于线程A拥有该变量的独享操作
(3)线程A执行i++操作,经历上述说的(read、load、use、assign、store、write)指令之后;操作完成执行unlock释放锁定的这个内存变量
(4)线程B这个时候再去主内存读取的时候,发现未被锁定,就可以重新读取了
小陈:牛逼啊老王,听君一席话,白读十年书。献上我的掌声......
多线程并发在JMM操作带来的可见性问题
小陈:JMM以及它的8中基本操作我了解了,但是我发现了个问题,在多线程操作的时候可能会有数据不一致的问题,比如我画个图讲一下我的想法:
线程A和线程B同时执行共享变量x的++操作;线程B在线程A将x=1的值刷入主内存之前读取x=0,这样就会导致数据错了。
老王:很好,看来小陈很聪明啊,理解得很快啊
小陈:嘿嘿,一般般啦...
老王:你说的这个问题,正是多线程并发操作的可见性问题。
小陈:我知道多线程并发操作的时候,会有可见性、有序性、原子性的问题,这种情况会导致可见性问题我知道了,但是有序性和原子性是什么?什么时候会发生的?会导致什么问题?
老王:哈哈,你有这个好奇心很好,我们今天先说这么多,可见性、有序性、原子性的问题我们留到下一篇再说,别急哦,一点点慢慢来,啧啧..