深度学习Java之内存模型「译」

Java的内存模型定义了Java虚拟机如何和计算机物理内存进行交互。Java虚拟机是一体化的计算机模型,所以它自然也包含了内存模型。

如果你要设计运行稳健的并发程序,理解Java内存模型是非常必要的。Java内存模型定义了不同线程之间变量读写的可见性以及如何同步访问共享变量。

最原始的Java内存模型设计的很不足。后来到了Java1.5版本就重新制定了,这个版本的内存模型一直延续到了现在。

深入Java内存模型

Java内存模型内部分为线程堆栈和堆两种区域。下图粗略了表示了这种结构。

Java虚拟机中的每个线程都有它自己的线程栈。线程栈记录了当前线程到达当前执行点所经历的一系列方法调用信息。暂且把它称之为【调用栈】。随着线程不停地执行代码,调用栈一直不停的改变。

线程栈记录了正在运行的调用栈里所有方法的局部变量信息。线程只能访问它自己的线程栈。局部变量仅对创建它的线程有效,其它线程是完全看不见的。甚至当两个线程执行的是同一段代码逻辑,它们也会分别在自己的线程栈里创建这些局部变量,每个线程都有它自己的局部变量,互不干扰。

所有原生类型(boolean, byte, short, char, int, long, float, double)的局部变量都是完全存储在当前的线程栈里面,别的线程完全看不到。线程之间可以通过拷贝来分享原生类型的变量,但是无法直接共享。

堆包含了Java应用程序创建的所有对象,不管这些对象是被哪个线程创建的。这些对象包含了所有原生类型的包装类型。无论这些被创建的对象是被赋值给了哪个局部变量还是挂在了某个对象的成员变量上,它们都存储在堆里面。

下图展示了Java虚拟机里面的调用栈和局部变量以及存在堆里面的所有对象。

局部变量可以是原生类型,这样它就完全存在了栈里面。

局部变量也可以是对象的引用,这种情况下对象的引用也就是局部变量本身是存在栈里面的,而对象本身的内容则放在堆里面。

对象里有方法,这些方法又有局部变量。这些局部变量也存在栈里面,尽管这些方法所属的对象是放在堆里面的。

对象的所有字段同对象一样都是放在堆里的,不论这个字段是原生类型还是对象的引用类型。

静态变量和类定义对象一样也是放在堆里的。

堆里的对象可以被所有线程访问,只要这些线程有相应对象的引用。线程如果可以访问一个对象,那一定就可以访问这个对象的成员字段。如果两个线程同时访问同一个对象的同一个方法,那么它们会同时访问对象的字段,当时每个线程都会创建属于自己的局部变量。

具体请看下面这张图

两个线程都有一些局部变量。其中局部变量2指向了堆里面的对象3。这两个线程都有属于自己的指向同一个对象的引用变量。这些引用都是局部变量,都存放在各自的线程栈上,尽管它们都指向同一个对象。

注意到这个共享的对象3持有对象2和对象4的引用作为它的内部成员。通过成员变量引用, 这两个线程都可以访问对象2和对象4。

图中两个线程栈里面的局部变量1分别指向了堆里的不同对象。理论上堆里的所有对象都可以被两个线程访问,但是就图上所示,每个线程都只有一个对象的引用。

上面这张图可以使用下面的代码来表示

如果两个线程同时执行run方法,就呈现出了上面那张图。run方法调用methodOne,methodOne又调用methodTwo。

methodOne定义了一个原生的局部变量和一个对象引用局部变量。两个线程在执行methodOne的时候都会在各自的栈上创建局部变量1和局部变量2,它们互不干扰,一个线程是看不到另外一个线程对局部变量做的任何修改的。

两个线程在执行methodOne时还会创建各自的局部变量2,然后这两个变量都指向堆里的同一个对象。代码设置局部变量2指向一个静态变量引用的对象,这个静态变量是唯一的,也存放在堆里。结果就是这两个局部变量都指向同一个又静态变量指向的ShareObject对象,ShareObject这个对象实例就存放在堆里,就是图中的对象3。

注意到MyShareObject的两个成员变量都是long类型的。因为它们是成员变量,所以也跟对象一起放在堆里。只有局部变量才会放在栈上。

硬件内存结构

现代的硬件内存结构和Java内存模型是不一样的。想要理解Java内存模型是如何同物理内存交互的话,先把硬件内存结构搞懂非常重要。这一节我们来看看硬件内存结构,接下来我们再看Java内存模型是如何与之交互的。

看图

现代计算机一般都有多个CPU,每个CPU又有多个核。所以它们往往可以同时跑多个线程。每个CPU同一时间都可以跑一个线程。这就意味着如果你的Java程序是多线程的,每个CPU都会同时跑着一个线程。

每个CPU内部都有一堆基本的寄存器。CPU和这些寄存器交互要比和内存交互快得多。

每个CPU都有一个缓存层,现代的CPU一般都是特定大小的缓存层。CPU访问这些缓存要比访问内存快,但是还是比不上寄存器。有些CPU可能会有多个缓存层,不过这不影响我们理解Java内存模型是如何同内存进行交互的。我们只要知道CPU有这样一个缓存层就够了。

计算机的主内存所有的线程都可以访问,它的容量往往比CPU缓存大得多。

当CPU要读主内存时,它会首先读取一小块内存到CPU缓存里,接着又会读取缓存的一小块到寄存器里,然后就可以继续操作了。当CPU要写主内存时,它会将寄存器里的值Flush到缓存里,后面又会将缓存里的值Flush到主内存。

当CPU想用这些缓存存点别的什么东西时会先把缓存里的数据会刷回到主内存。CPU缓存可以在从主存加载数据的同时回写数据到主存,这两个操作对象是CPU缓存的不同部分。CPU缓存没必要一次性读写整个缓存区域。一般来说CPU缓存更新的单位是【缓存行】,英文是cache lines。一部分缓存行从主存加载数据,另一部分缓存行回写数据到主存。

Java内存模型和硬件内存结构的异同

前面提到,Java内存模型和硬件内存结构是不一样的。硬件内存结构是不会区分线程栈和堆的。在硬件上,线程栈和堆都放在主内存里,还有一些甚至会在CPU缓存和寄存器里。如下图所示

当对象和对象可以放在不同的存储地方的时候,问题就来了。

线程修改共享变量的可见性

读写共享变量的竞态条件

解下来我们逐个分析

共享对象的可见性

如果多个线程共享一个对象,而这个对象又没有使用volatile修饰也没有任何同步控制的话,一个线程对共享变量的修改是可能不会立即被其它线程看到的。

试想一开始对象是在主存里创建的,然后CPU将对象加载到CPU缓存,在缓存里修改了这个对象而没有立即将缓存写会主存。那这个修改过的对象是不能够被运行在其它CPU上的线程看到的。结果就是每个线程都有这个对象的一个拷贝,分别放在不同的CPU缓存里。

下图能很好的表现这种情况。运行在左边CPU上的线程将count变量加载到CPU缓存中,然后将count修改成了2,而右边CPU上跑的线程则看不到这种改变。因为修改过的缓存行没有同步刷回主内存。

为了解决这个问题就必须用到volatile关键字,volatile关键字会确保读操作会直接读取主内存,对变量的修改也是立即回写主内存。

竞态条件

多个线程共享一个对象,并且同时对这个对象进行修改的时候,竞态条件就产生了。

假设线程A将共享变量count读到CPU缓存,同时线程B也将共享变量count读到另一个不同的CPU缓存,然后同时对count进行加1操作,如此count变量被更新了2次。

如果这两个加1操作是串行执行的话,那么当count变量回写内存的时候,值就是value+2

可现在这两个操作没有进行适当的同步控制,也就是说不是串行的。不管是哪个线程先回写主存,最后的值总是value+1,而不是value+2。下面这张图显示了这种竞态条件。

解决这种问题,你可以使用Java的synchronized关键字。synchronized关键字保证同一时间只有一个线程可以进入临界区。同步块同样也可以保证块内的读变量都是直接读主存,变量修改在退出同步块的时候会立即回写到主存,不管这个变量有没有使用volatile修改都一样。

阅读相关文章,关注微信公众号/知乎专栏/头条号【码洞】

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

推荐阅读更多精彩内容