Java内存模型

Java内存模型

  为了屏蔽各种硬件和操作系统的内存访问差异,实现Java在不同平台下都能达到一致的内存访问效果,而定义出的一种内存模型规范。

一、主内存和工作内存

Java内存模型的主要目标是为了定义程序中各个变量的访问规则(虚拟机中读写变量....这些变量包括实例字段、静态字段、构成数组对象的元素,但不包括线程私有而不存在竞争的方法参数和局部变量)。Java内存模型并没有现在执行引擎使用处理器的特定寄存器或者缓存来和主内存进行交互,也没有限制编译器调整代码顺序执行这一类优化措施。

  Java内存模型规定所有变量都存储在主内存中,除此之外,每条线程拥有自己的工作内存(线程的工作内存中保存的是执行时候需要使用的变量从主内存中拷贝的副本),且线程执行的时候对变量的读写操作都是在自己的工作内存中进行,而不是直接从主存中读或者写。(不同的线程之间不能访问彼此的工作内存,线程之间的访问通信均需要通过主内存来完成)

二、内存建交互操作

  1、上面区分了主内存和工作内存二者的关系和区别,那么实际上JMM中定义了下面几种操作来完成变量从主内存拷贝到工作内存、然后从工作内存同步写回主内存中。而且这些操作都是原子性的(64位的double类型和龙类型可能会被拆分成32位来进行操作)

  ①lock(锁定):作用与主内存中的变量,将一个变量标识为线程独占的状态;

  ②unlock(解锁):作用于主内存中的变量,将一些被线程独占锁定的变量释放,从而可以被其他线程占有使用;

  ③read(读取):作用于主内存中的变量,将变量的值从主内存中传输到工作内存中,以便后去的load操作使用;

  ④load(载入):作用于工作内存中的变量,将上面read操作从主内存中获取的值放在自己的工作内存中的变量副本中;

  ⑤use(使用):作用于工作内存中的变量,将工作内存中的变量值传递给执行引擎,当虚拟机需要使用变量的值的时候回执行这个操作;

  ⑥assign(赋值):作用于工作内存中的变量,将一个从执行引擎接受到的值赋给工作内存中的该变量,当虚拟机遇到给变量赋值操作的指令时候执行;

  ⑦store(存储):作用域工作内存中的变量,将工作内存中的变量值传送回主内存中,以便后续的write操作使用;

  ⑧write(写入):作用于主内存中的变量,将store操作从工作内存中得到的变量的值写回主内存的变量中。

  2、对于一个变量而言:如果要从主内存复制到工作内存,就需要顺序执行read和load操作;如果需要将其写回主内存,就需要顺序的执行store和write操作。(这两种情况是需要按序执行的,但是不限制必须连续执行,在他们操作之间可以执行其他指令)

  3、一些其他的规则

  ①不允许read和load、store和write操作中的一个单独出现(即不允许将一个变量从主内存中读取但是工作内存不接受、或者是从工作内存写回但是主内存不接受的情况);

  ②不允许一个线程丢弃最近使用的assign操作(即变量在工作内存中改变了之后需要将变化同步回主内存之中);

  ③不允许一个线程在没有发生assign操作的时候,就将数据从工作内存同步回主内存之中;

  ④一个新的变量只能在主内存之中产生,不允许在工作内存中直接使用一个未被初始化的变量(对这个变量执行load或assign操作),即对一个变量执行use和store操作之前必须执行了assign和load操作;

  ⑤如果对一个变量进行lock操作,将会清空工作内存中该变量的值,在执行引擎使用这个变量之前,需要重新执行load和assign操作;

  ⑥如果一个变量事先没有被lock操作锁定,那么不允许对其执行unlock操作,也不允许某一个线程去unlock一个被其他线程lock住的变量;

  ⑦在对一个变量执行unlock之前,必须将这个变量的值同步回主内存中。

三、volatile变量

1、volatile是JVM提供的轻量级同步机制

  当一个变量被定义为volatile之后,会具备下面两种特性

  a)保证此变量对于其他所有线程的可见性(当某条线程改变了这个volatile的值后,其他的线程能够得知这个变化)。这里需要指出:

  ①volatile变量存在不一致的情况(虽然各个线程中是一致的,但是这种情况的原因是在使用变量之前都需要刷新,导致执行引擎看不到不一致的情况,那么在各个线程中看到的自然就是一致的);

  ②Java中的运算不是原子的,导致基于volatile变量在并发情况下的操作不一定就是安全的,如同下面的例子:使用10个线程对volatile类型的变量count进行自增运算操作,然后观察运行的计算结果发现并不是期望的100000,而是小于该值的某个其他值

1package cn.test.Volatile; 2 3import java.util.ArrayList; 4import java.util.List; 5 6publicclass TestVolatile02 { 7volatileintcount = 0; 8void m(){ 9count++;10    }1112publicstaticvoid main(String[] args) {13finalTestVolatile02 t =new TestVolatile02();14List threads =newArrayList<>();15for(inti = 0; i < 10; i++){16threads.add(newThread(new Runnable() {17                @Override18publicvoid run() {19for(inti = 0; i < 10000; i++){20                        t.m();21                    }22                }23            }));24        }25for(Thread thread : threads){26            thread.start();27        }28for(Thread thread : threads){29try {30                thread.join();31}catch (InterruptedException e) {32// TODO Auto-generated catch block33                e.printStackTrace();34            }35        }36        System.out.println(t.count);37    }38}

  ③实际上使用javap反编译之后的代码清单,我们查看m()方法的汇编代码,发现count++实际上有四步操作:getfield->iconst_1->iadd->putfield,分析上面程序执行和预期结果不同的原因:getfield指令把count值取到栈顶的时候,volatile保证了count的值在此时是正确的,但是在执行iconst_1和iadd的时候,其他线程可能已经将count的值增大了,这样的话刚刚保存在操作数栈顶的值就是过期的数据了,所以最后putfield指令执行后就可能把较小的值同步到主存当中了。

void m();

    Code:

      0: aload_0

      1: dup

      2: getfield      #2// Field count:I5: iconst_1

      6: iadd

      7: putfield      #2// Field count:I10:return


  ④上面的代码要想保证执行正确,我们还需要在执行m方法的时候使用锁的机制来保证并发执行的正确性,可以使用synchronized或者并发包下面的原子类型。下面需要使用这两种加锁机制的场合

□运算结果不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值

  □变量不需要与其他的状态变量共同参与不变约束。

  ⑤看下面的代码,就比较适合volatile。使用volatile类型的变量b能够在其他线程调用endTest修改b的值之后,所有doTest的线程都能够停下来。

1package cn.test.Volatile; 2 3import java.util.concurrent.TimeUnit; 4 5publicclass TestVolatile01 { 6volatilebooleanb =true; 7 8void doTest(){ 9System.out.println("start");10while(b){}11System.out.println("end");12    }1314void endTest() {15b =false;16    }1718publicstaticvoid main(String[] args) {19finalTestVolatile01 t =new TestVolatile01();20newThread(new Runnable() {21            @Override22publicvoid run() {23                t.doTest();24            }25        }).start();2627try {28TimeUnit.SECONDS.sleep(1);29}catch (InterruptedException e) {30            e.printStackTrace();31        }32        t.endTest();33    }34}

  b)禁止指令重排序优化

  普通变量只能保证在程序执行过程中所有依赖赋值结果的地方都能获取到正确的结果,但是不能保证变量赋值操作的顺序与程序中的代码执行顺序一致。而指令的重排序就可能导致并发错误的结果。使用Volatile修饰就可以避免因为指令重排序导致的错误产生。

  c)java内存模型中对volatile变量定义的特殊规则。假定T表示一个线程,V和W分别表示volatile型变量,那么在进行read、load、use、assign、store和write操作时需要满足如下规则:

  ①只有当线程T对变量V执行的前一个动作为load时,T才能对V执行use;并且,只有T对V执行的后一个动作为use时,T才能对V执行load。T对V的use,可以认为是和T对V的load。read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对V修改后的值)。

  ②只有当T对V的前一个动作是assign时,T才能对V执行store;并且,只有当T对V执行的后一个动作是store时,T才能对V执行assign。T对V的assign可以认为和T对V的store、write相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程看到自己对V的修改)。

  ③假定动作A是T对V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对V的read或write动作;类似的,假定动作B是T对W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对W的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令的重排序优化,保证代码的执行顺序与程序的顺序相同)。

四、原子性、可见性与有序性

  1、原子性:由Java内存模型来直接保证的原子性变量操作(包括read,load,assign,use,store,write),可以认为基本数据类型的访问读写都是具备原子性的(尽管double和long的读写操作划分为两次32位的操作来执行,但是目前商用虚拟机都将64位的数据读写操作作为原子性来对待)

  2、可见性:当一个线程修改了共享变量的值,其他线程能够立即得到这个修改的状况。除了volatile,Java还有两个关键字能实现可见性,synchronized和final。同步块的可见性是由“对一个变量执行unlock操作之前,必须把此变量同步回主内存中(执行store和write操作)”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么其他线程中就能看见final字段的值。

  3、有序性:Java提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。

五、先行发生原则(happens-before)

  1、先行发生原则:是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A发生在操作B之前,那么操作A产生的结果能被操作B观察到(这个结果包括修改内存中共享变量的值、发送通信消息、调用某个方法等等)。

  2、下面是Java内存模型中的一些先行发生关系,这些happens-before关系不需要任何的同步操作就已经存在,可以在编码中直接使用,如果两个操作之间的关系不在此列,并且无法从下列规则中推导出来的话,他们就没有顺序性保证,那么虚拟机就可以对其进行重排序优化。

  ①程序次序规则:在一个线程内,按照程序控制流(包括分支、循环)顺序执行。

②管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。

  ③volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。

  ④线程启动规则:Thread对象的start方法先行发生于此线程的每个动作。

  ⑤线程终止规则:线程中的所有操作都先行发生于此线程的终止检测,我们可以通过Thread.join()方法结束/Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  ⑥线程终端规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

  ⑦对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize方法的开始、

  ⑧传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。

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

推荐阅读更多精彩内容

  • 除了充分利用计算机处理器的能力外,一个服务端同时对多个客户端提供服务则是另一个更具体的并发应用场景。衡量一个服务性...
    胡二囧阅读 1,340评论 0 12
  • 注:此文是我在读完周志明老师的深入理解Java虚拟机之后总结的一篇文章,请阅读此书获取更加详细的信息. 在介绍Ja...
    AlstonWilliams阅读 386评论 0 1
  • 目录 一、Java 内存模型的主要目标二、主内存和工作内存三、内存件的交互操作四、对于 volatile 型变量的...
    panning阅读 1,711评论 0 8
  • 一、QA 计算机硬件中高速缓存的作用是什么? 内存读写速度与处理器运算速度相比有几个数量级的差距,所以现代计算机都...
    tangyu_tyty阅读 298评论 0 0
  • 随着计算机系统的发展,多任务处理器系统在现代计算机操作系统中已经是一个必不可少的组成部分了。在很多情况下,如果想要...
    LeonardoEzio阅读 997评论 0 1