《java并发编程实战》之java内存模型

“如欲征服java并发,需先征服java内存模型,如欲征服java内存模型,需先征服计算机内存模型” -aworker.

大佬讲话
大佬讲话

咳!咳!,大家都记好笔记了吧。虽然我不是什么大佬,但是这句话说的还是没有毛病的。不了解java的内存模型,就不会从跟不上理解java并发的一些行为和机制,而java内存模型毕竟是jvm模拟出来的一部分,其底子还是建立在现代计算机的物理内存模型上来的,所以我们就按照现代计算机的物理内存模型、java内存模型的顺序来仔细介绍,为彻底了解java并发机制打下底子。
现代计算机的物理内存模型:


现代计算机的物理内存模型
现代计算机的物理内存模型

现在计算机最少的都是应该是两核心了,当然我们也经常在买个人电脑的时候听过四核四线程、四核八线程等,可以说现在个人电脑标配都是四核心了,为了方便上图只是列举了2个核心。现代计算机的内存在逻辑上还是一块。有人可能问不对啊,我电脑就插了两块内存,但是操作系统会把两块内存的地址统一抽象,比如每一块的内存是2048MB地址是000000000000-011111111111MB,两块就是0000000000000-0111111111111MB,操作系统会统一编址。所以整体上看还是一块内存。因为CPU的操作速度太快,如果让CPU直接操作内存,那么久是对CPU资源的一种巨大浪费,为了解决这个问题现在计算机都给CPU加上缓存,比如一级缓存,二级缓存,甚至三级缓存。缓存速度比内存快,但是是还是赶不上CPU的数据级别,所以在缓存和CPU之间又有了register,register的存储速度比缓存就快了好多了。
存储速度上有如下关系:
register > 一级缓存 > 二级缓存 > ... > n级缓存 > 内存
容量上一般有如下关系:
内存 > n级缓存 > ... > 二级缓存 > 一级缓存 > register
之所以可以用缓存和register来缓解CPU和内存之间巨大的速度差别是基于如下原理:

CPU访问过的内存地址,很有可能在短时间内会被再次访问。
所以,比如CPU访问了地址为0x001fffff的内存地址,如果没有缓存和register,那么CPU再下次访问这个内存地址的时候就还要去内存读,但是如果有缓存,缓存会把CPU访问过的数据先存储起来,等CPU待会再找地址为0x001fffff的内存地址时候,发现其在缓存中就存在了,那么好了,这就不用在访问内存了。速度自然就提升了。这就涉及到计算机组成原理的知识了,如果想了解可以google一下,这里就不在做更深的介绍了到这里就够用了。
了解现代计算机物理内存模型工作原理后,那么再理解多线程开发中最关系的三个概念就有的放矢了。先介绍下三个概念:

  1. 操作原子性:一个操作要么全做,要么全不做,那么这个操作就符合原子性。比如你给你老婆银行卡转500块钱,就包括两个操作,自己账户先减500,你老婆账户加500。但是这个转账操作应该满足原子性。如果银行只执行了你自己账户的扣钱操作,没有执行给你老婆账户的加钱操作。丢了500块钱是小事,被老婆大人罚跪搓衣板可就不得了了。所以你自己账户减钱,老婆账户加钱,这两个操作要么都做了,要么都别做。例如如下操作:
a = a + 1; 

结合我们上述的现代计算机的内存模型,计算机执行a=a+1时候会分成三个原子性操作:

  1. 把a的值(比如4)从内存中取出放到CPU的缓存系统中
  2. 从缓存系统中取出a的值加1(4+1)得到新结果
  3. 把新结果存回到内存中

一个“a=a+1”操作计算机中被拆分成三个原子性操作,那么完全可以出现CPU执行完1.操作后,去执行别的操作了。这就是并发操作原子性问题的根本来源。

  1. 操作有序性:例如如下代码:
public class A {
   public int a;
   public boolean b = false;

   public void methodA(){
       a = 3;
       b = true;
       a = a + 1;
   }

   public void methodB(){
       a = 3;
       b = (a == 4);
       a = a + 1;
   }
} 

methodA方法代码先经过java编译器编译成字节码,然后字节码然后被操作系统解释成机器指令,在这个解释过程中,操作系统可能发现,咦?在给变量b赋值为true后又操作了a变量,干脆我操作系统自己改改执行顺序,把对a变量的两个操作都执行完,然后再执行对b的操作,这就叫指令重排序。这样就会节省操作时间,如下图没有进行指令重排序时:

没有指令重排序
没有指令重排序

图中CPU和缓存系统要进行9次通信,缓存系统和内存要通信7次,假设cpu和缓存系统通信一次用时1ms,缓存系统和内存通信一次用时10ms,那么总用时 9乘1 + 7乘10 = 79ms。经过指令重排序后,总共用时 6乘1 + 6乘10 = 66ms 如下图所示:
有指令重排序
有指令重排序

经过指令重排序的确可以提程序运行效率,所以现代计算机都会对指令进行重排序,但是这种重排序也不是无脑重排序,重排序的基础是前后语句不存在依赖关系时,才有可能发生指令重排序。所以A类的methodB方法不会发生指令重排序。指令重排序在单线程环境里面这不会有什么问题,但是多线程中就可能发生意外。比如线程1中执行如下代码:

instance.methodA();

另一个线程2执行如下代码:

while(instance.a != 4){ //a只要不等4,线程就让出CPU,等待调度器再次执行此线程
   Thread.yield(); //让出CPU,线程进入就绪态
}
System.out.print(instance.b);

其中instance是A类的一个实例。如果线程1 发生了指令重排序, 那么这线程2的打印结果很有可能是false,这就和我们对代码的直观观察结果出处很大。如果线上产品出错的原因是指令重排序导致的,几乎不能可能排查出来。

  1. 操作可见性 :
    在“操作有序性” 中的线程线程2 ,还有可能会没有任何输出结果。因为线程2 要想有输出必须要满足instance.a =4,但这是在线程1中调用methodA 方法后instance.a 的值才为4 。而要想让线程2 看到这个新值,必须要把线程1的修改及时写回内存, 同时通知线程2 存在缓存系统中的instance.a值已经过期,需要去内存中获取最新值。如果我们的类A和线程1、线程2调用的代码没有特殊的声明,那么操作系统不能保证上述过程一定发生。即可能发生线程1对instance.a的修改对线程2不一定可见,这就是操作的可见性问题。

java多线程的所有问题都植根于“操作原子性”、“操作有序性”、“操作可见性”而引发的。

上面介绍了现代计算机的内存模型以及其引起的在并发编程的三个问题,下面来介绍下java的内存模型。java为了实现其夸平台的特性,使用了一种虚拟机技术,java程序运行在这虚拟机上,那么不管你是windows系统,linux系统,unix系统,只要我java虚拟机屏蔽一切操作系统带来的差异,向java程序提供专用的、各系统无差别的虚拟机,那么java程序员就不需要关心底层到底是什么操作系统了。对于int类型的变量其取值范围永远是 -2^31 -1 至 2^31,即4个字节。但是对C\C++,这个操作系统的int可能是4字节,那个可能是8字节。C++程序员跨平台写代码,痛苦异常。这个给我们编程带来极大方便的虚拟机就是大名鼎鼎的JVM(Java Virtual Machine)。既然是虚拟机那么就需要模拟真正物理机的所有设备,像CPU,网络,存储等。和我们程序员最密切的就是JVM的存储,这就是java内存模型(Java Memory Model 简称JMM)。有别于我们真实的物理存储模型,JMM把存储分为线程栈区和堆区。在JVM中的每个线程都有自己独立的线程栈,而堆区用来存储java的对象实例。java中各种变量的存储有一下规则:

  1. 成员变量一定存储在堆区。
  2. 局部变量如果是基本数据类型存储在线程栈中,如果是非基本数据类型存储,其引用存储在线程栈中,但具体的对象实例还是存储在栈中。

因为java内存模型是在具体的物理内存模型的基础上实现的,并且为了运行效率,java也支持指令重排序。所以java并发编程也有“原子性”、“有序性”、“可见性”三个问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容