Java内存模型

        先从计算机的硬件效率说起,CPU的计算速度比内存快几个数量级,为了平衡CPU和内存之间的矛盾,引入的高速缓存,每个CPU都有高速缓存,甚至是多级缓存L1、L2和L3,那么缓存与内存的交互需要缓存一致性协议。那么最终处理器、高速缓存、主内存的交互关系如下:

 

    Java的内存模型(Java Memory Model,简称JMM)也定义了线程、工作内存、主内存之间的关系,非常类似于硬件方面的定义:

  

Java虚拟机运行时内存的区域划分

    方法区:存储类信息、常量、静态变量等,各线程共享

    虚拟机栈:每个方法的执行都会创建栈帧,用于存储局部变量、操作数栈、动态链接等,虚拟机栈主要存储这些信息,线程私有

    本地方法栈:虚拟机使用到的Native方法服务,例如c程序等,线程私有

    程序计数器:记录程序运行到哪一行了,相当于当前线程字节码的行号计数器,线程私有

    堆:new出的实例对象都存储在这个区域,是GC的主战场,线程共享。

    所以对于JMM定义的主内存,大部分时候可以对应堆内存、方法区等线程共享的区域,这里只是概念上对应,其实程序计数器、虚拟机栈等也有部分是放在主内存的,具体看虚拟机的设计。

 

    线程A、B同时去读取主内存的count初始值存放在各自的工作内存里,同时执行了自增操作,写回主内存,最终得到了错误的结果。

 

造成这个错误的本质原因:

    (1)、可见性,工作内存的最新值不知道什么时候会写回主内存

    (2)、有序性,线程之间必须是有序的访问共享变量,我们用“视界”这个概念来描述一下这个过程,以B线程的视角看,当他看到A线程运算好之后,把值写回之内存之后,马上去读取最新的值来做运算。A线程也应该是看到B运算完之后,马上去读取,在做运算,这样就得到了正确的结果。

 

接下来,我们来具体分析一下,为什么要从可见性和有序性两个方面来限定。

    给count加上volatile关键字,就保证了可见性。

private volatile int count = 0;

    volatile关键字,会在最终编译出来的指令上加上lock前缀,lock前缀的指令做三件事情

    (1)、防止指令重排序(这里对本问题的分析不重要,后面会详细来讲)

    (2)、锁住总线或者使用锁定缓存来保证执行的原子性,早期的处理可能用锁定总线的方式,这样其他处理器没办法通过总线访问内存,开销比较大,现在的处理器都是用锁定缓存的方式,在配合缓存一致性来解决。

    (3)、把缓冲区的所有数据都写回主内存,并保证其他处理器缓存的该变量失效

    既然保证了可见性,加上了volatile关键词,为什么还是无法得到正确的结果,原因是count++,并非原子操作,count++等效于如下步骤:

   (1)、 从主内存中读取count赋值给线程副本变量:

            temp=count

    (2)、线程副本变量加1

            temp=temp+1

    (3)、线程副本变量写回主内存

            count=temp

    就算是真的严苛的给总线加锁,导致同一时刻,只能有一个处理器访问到count变量,但是在执行第(2)步操作时,其他cpu已经可以访问count变量,此时最新运算结果还没刷回主内存,造成了错误的结果,所以必须保证顺序性。

    那么保证顺序性的本质,就是保证同一时刻只有一个CPU可以执行临界区代码。这时候做法通常是加锁,锁本质是分两种:悲观锁和乐观锁。如典型的悲观锁synchronized、JUC包下面典型的乐观锁ReentrantLock。

    总结一下:要保证线程安全,必须保证两点:共享变量的可见性、临界区代码访问的顺序性。

 

文章来源:https://my.oschina.net/u/1778239/blog/1610185

转载于:https://www.cnblogs.com/ryjJava/p/9448330.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值