Java 并发编程 ④ – Java 内存模型


原文地址:Java 并发编程 ④ – Java 内存模型

转载请注明出处!

往期文章:

  • Java 并发编程根底 ① – 线程
  • Java 并发编程 ② – 线程生命周期与状况流通
  • Java 并发编程 ③ – ThreadLocal 和 InheritableThreadLocal 详解

n # % ?

Java内存模型(Java Memory Model ,Jl C ? S ? a . PMM)便是一种符合内存模型标准的,屏蔽了各种硬件和操作体系的拜访差异的,确保了Java程序在各种平台下对内存的拜访都能得到一致效果的机制及标准

JMM与Java内存区域是两个简略混淆的概念,这两者既有不同又有联z ; & T ? F h络:

  • 差异

两者是不同的概念层次A } M C 3 [ 0 oJava 内存模型是笼统的,他是用来描[ b c P | i _ i }绘一组规矩,经过这个规矩来操控各个变量的拜访办法,环绕原子性、有序性、可见性等打开的。而Java运转时内存的区分是W 9 x C详细的,是JVM运转Java程序时,必要的内存区分。

  • 联络

都存{ B E在私有数据区域和同享数据区域。一般来说,JMM中的主内存 t , A M归于同享数据区域,他是包含了堆和办法区;相同,JMM中的本地内存归于私有数据区域,包含了程序计数器、本地办法栈、虚拟机栈。

在学习Java 内存模型时,咱们经常会说到3个特性:{ l P B : 9 } `

  • 可见性 – Visibility
  • L & 原子性 – Atomicity
  • 有序性 – Ordering

Java内存模型便是环绕u W 8 9 u 9 3着在并发过程中怎样处理这3个特性来K @ ? Z F e Y N树立的。本文也会依照这三个特性叙述。

一、Java 同享变量的内存可见性问题

在评% 7 $ h论之前,需求先重温一下,JVM运转时内存区域:

Java 并发编程 ④ - Java 内存模型

线程私有变量不会在线程之间同享,也就不会有内存可见性的问题,也不受内存模型U G S v e / U的影响。而在堆中的变量是D & * W同享的,这一块的数据也称为同享变量,内存可见性问题针对的便是同享变量。


好了,弄清楚问题的主体之后,咱们再来思考一个问题。

为什么堆上的变量会存在内存可见性的问题呢?

JMM对` , ; N x x . & V硬件层面缓存拜访的笼统

其实,这就要涉及到计算机硬件的缓存拜访操作了。

现代计算机中,处理器上的寄存器的读写的速度Y F $比内存快几个数量级,为了处理这种速度对立,在它们之间加入了高速缓存。

Java 并发编程 ④ - Java 内存模型

Java的内存拜访操作与上述的硬件缓存具有很高的可比性:

Java内存模型中,规定了:

  • 一切的变量都存储在主内存中。
  • 每个线程还有自己的作业内存,存储了该线程以读、写同享变量的副本。
  • 本地内存(或许叫作业内存)是Java内存模型的一个笼统概念,并不真实存在。它涵盖了缓存、写缓冲区、寄t G @ 4存器等。
  • 线程只能直接操作作业内存中的变量,不同线程之间的变量值传递需求经过主~ N 0内存来完成。
Java 并发编程 ④ - Java 内存模型

从笼统的角度来说,JMM界说了线程和主内存之间的笼统关系

依照上述关于JMM的描绘,当一个线程操作同享变量时,它首先从主内存复制同享变量到自己的作业内存,然后对作业内存里的变F R b # z & : w量进行处理,处理完后将变量值更新到主内存。

CachP l W B 7e(作业内存)的存在就会带来同享变量的内存不行见的问题(也能够叫做缓存一致性问题),详细能够看下面的比如:

  • 假定现在主内存中有同享变量X=0;

  • 线程A首先获取同享变量} r y j ZX的值,因为Cache中没有射中,所以去加载主内存中变量X的值,把X=0的值缓存到作x = W 1 _ | P j业内存中,线程A履行了修正操作X++,然后将其写入作业内存中,而且改写到主内存中。

Thread-A作业内存中X=1
主内存中X=1
  • 线程P | t $B开端获取同享? R 2 P * 6变量,因为Cache没有射中,所以去加载主内) * 9 2 g 5 G [ f存中变量X的值,把X=1的值缓存到作业内存中。然后线程B履行了修正操作X++,然后将其写入作业内存中,而且改写到主内存中。
Thread-B作业内存中X=2
Thread-A作业内存中X=1
主内存u @ f ^ 6 S u中X=2

明明. ^ % ; ` (线程B现已把X的值修正为了2,为何线程A获取的仍是1呢?这便是同享变量的内存不行见问题,也便是线程B写入的值对线程A不行见。

怎样确保内存的可见性

那么怎样确保内存的可见性,主要有三种完成办法:

  • volati@ * 7le 关键字

    该关键字能够确保对一个变量的更新对其他线程立刻可见。当一个变量被声明为R h svolatile时,线程@ ; D : 3 1在写入变量时不会把值缓存在寄存器或许其他地方,而是会把值改写回主W & I t L内存

  • sychronized 关键字

    一个线程在获取到监视v R p m & m M X器锁以后才干进入 synchronized 操控的代码块,一旦进入代码块,首先,该线程关于同享变量的缓存就会失效,A X i ` ! ? 5因此 synchronizeu i `d 代码块中关于同享变量的读取需求从主内存中重新获取,也就能获取到最新的4 7 . L I _ K

    退出代码块的时分,会将该线程写缓冲区中的数据刷到主内存中,所以在 synchronized 代码块之前或 synchronized 代码块中关于同享变量的操作随着该线程退出 synchronizef 4 z k 9 g 4 –d 块,会立即对其他线程可见(当然条件是线程会去主内存读取最新值)。

  • final 关键字

    在目) _ R J 7标的结构办法中设F 4 ! r ! X + n置 final 属性,同时在目标初始化完成前,不要将此目G N C B 7 J 2 _ l标的引证写入到其他线程能够拜访到的地方k H U _ g Q(不4 A a S } m要让引证在结构函数中逸出)。假如这个条件满足,当其他线程看到^ 1 0这个目标的时分,那个线程一直能够看到正确初始化后的目标的 final7 o R y 0 / H 属性。(final 字段所引证的目标里的字段或数组元素可能在后续还会改动,若没有正确同步,其它线程也许不能看到最新改动的值,但必定g o U h能够看到彻H U , * , 8 0底初始化的目标或数组被 final 字段引证的那个时间的目标字段值或数组元素。)

    final 的场j 7 ` x #景比较偏,一般便是前面两种办法

    延伸链接:JSR-133:JavaTM 内存模型与线程标准p G W + t ` : B $

va m ~ iolatile 和 sychU Q V * _ @ ( / Hronized 是我以为比较重要的内容,会有单独的章节来讲。

二、原子性

JMM 内存交互操作

Java 内存模型界^ U , ?说了 8 个操作来完成主f m ; c ~ e内存和作业内存的交互操作

  • read:把一k h = ~ Y 8个变量的值从主内存传输到线程的作业内存中
  • load:在 read 之后履行,把 read 得到的值放入线程的作业内存的变量副本中
  • use:把线程的作业内R v ! ` = 8存中一个变量的值传递给履行引擎
  • assign:把一个从履行引擎接收到的值赋给作业内存的变量
  • store:把作业内存的一个变h o O量的值传送到主内存中
  • write:在 store 之后履行X k K M,把 store 得到的值放入主内存的变量中
  • lock:作用于主内存的变量,把7 E # g 7 k ]一个变量标~ : T . t I识成一条线程独占的状况
  • unlock: 作用于主内存的变量,把一个处于锁定状况的变量释放出来,释放后的变量才干够被其他@ ] g E [ q p M线程锁定。

JMM关于内存交互的界说规矩非常的严谨和繁琐,为了便利了解,Java设计团队将Java内存模型的操作简化为read、write、lock和unlock四种,但这只是语言描绘上的等价化简,Ja0 ? # 0 g Lva内存模型的根底设计并未改动。

JMM 关于原子性的规定

所谓n @ % O f 2原子性操作,是指履行一系列操作时,这些操作要么全部履行,要么全部不履行, = T 8 . I C不存在只履行其间一部分的状况9 G R # ] x C

J| z R E 8 N ^ i Sava 内存模型确保了 readloaduseassignstoreq T D jwritelockunlock 操作具有原子性,例如对一个 int 类型的变量履行 assign 赋值操作,这个操作便是原子性的。但是 Java 内存模型答应虚拟机将没/ I ! x有被 volatile 润饰的 64 位. a % @ )数据(longdouble)的读写操作区分为两u / A & e次 32 位的操作来进行,也便是说基本数据类型的拜访读写是原子性的,除了longdouble对错原子性的,loadstorereadwrite 操作能够不具 } U N } 9 { #备原子性。 在《深入了解JaR T U xva 虚拟机》书中提示咱们只需求知道有这么一回事,真的要用到这个常识点的场景非常稀有。

同享变量的原子性问题

这里E } ; R i k E `放一个很经典的比如,并发条件下y U ] ` L | u s的计数器自增。

/**
*内存模型三大特性-原子性验证对比
*
*@authorRichard_yyf
*/

publicclassAtomicExample{

privatestaticAtomicIntegeratomicCount=newAtomicInteger(V G X m B);

privatestaticintcount=0;

privatestaticvoidadd(){
atomicCount.incrementAndGet();
count++;
}

publicstW k { i B 4 gaticvoF ? M p + 9 3 d {idmain(String[]args){
finalintthreadSize=1000;
finalCountD1 ) j W 2 A h h (ownLo 4 yatchcountDownLatch=newCountDownLatf v { K O Nch(threadSize);
ExecutorServiceexecutP I O * Ior=Executors.newCachedThreadPool();
for(inti=0;i<threadSize;i++){
executor.execute(()->{
add();
countD_ r h o y H o 9 rownLatch.countDown(Y { 3 &);
});
}
System.out.println("atomicCount:"+atomicCounj w #t);
System.out.printlu T { * 8 T - E -n("count:"+count);

ThreadPoolUtil.tryReleasH E . U @ kePool(executor);
}
}

输出成果:

atomicCount:1000
count:997

能够看到,虽然有1000个线程履行了count++操作,终究得到的成果却不是预期的1000。

至于原因呢,? 2 i u便是因为count++这行代码,并不是一个原子性操作。o b q } S a能够借助下图帮助了解。

Java 并发编程 ④ - Java 内存模型

count++这个简略的操作根据上面的原理剖析,能够知道内存操作实践分} Z 7 : z x { E t为读写存三步;因为读写存这个整体的操作,不具备原子性,count被两个或多个线程读入了相同的旧值,读到线程内存当中,再进行写操作,再存回去,那么就可能呈现主内存被重复set同一个值的状况,如上图所示,两个线程进行了count++,实践上只进行了一次有效操作w + H O W L

怎样确保原子性

想要确保原子性,能够尝试以下几种办法:

  • CAS:运用基于CAS完成的原子操作类(例如AtomicInteger)
  • synchronized 关键字:能够运用synchronized 来确保限定临界区内操作的p 0 0 S &原子性。它对应的内存间交互q 6 o .操作为:lock 和 unlock,在虚拟机完成上对应的字节码指令为 monitorenter 和 monitorexit

前者是达观锁(读多写少场景),后者是失望锁(读少写多场景)

三、有序性

重排序

计算机在履行程序时,为了进步功能,编译器和处理器会对指令做重排U # : ( g _ k /

重排序由以下几种机制引起:

  • 编译器优化重排

    编译器在不改动单线程程序语义的条件下,能够重新安排句子的履行次序。

  • 指令并行重排

    现代处理器采用了指令级并行技术来将多条指令重叠履行。假如不存在数据依赖性(即后一个履行的句子无需依赖前面履行的句子的成果),处理器能S z –够改动句子对应的机器指令的履行次序。

  • 内存体系重排

    因为处理器运用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序履行,因为三级缓存的存在,导致内存与缓存的数据同步存% w O a p J =在时间差。

怎样确# 1 5 h g K F保有序性

Java内存模型答应编译器和l 0 G处理器对指令重排序以进步运转功能,而且只会对不存在数据依赖性的指令重排序。意] v B思便是说,在Java内存模型的规定下,对编译器和处理器来说,只需不改动程序的履行成果(单线程程序和正确同步了的多线程程序),编译器和处理器怎样优化都行。 在单线程下,能够确保重排序优t 4 M ` #化之后终究履行的成果与程序S 1 G g /次序履行的成果一致(咱们常说的as-if-serial语义),但是在多线程下就会存@ ` E 5 a #在问题。

重排序在多线程下会导致非预期的程序履行成果,想要确保可见性,能够考虑以下完成办法:

  • volatile

    volatile产生内存屏障,禁止指令重排序

  • synchronized

    确保每个时间只有一个线程进入同步代码块,适当于是让线程次序履行同步代码。

小结

Java内存模型的一系列运转规矩看起来有点繁琐,但总结起来,是环绕? { T原子性、可见性、有序性特征树立。归| L h * E ` R D 根究底,是为完成同享变量的在多个6 D d | K @ 2 q线程的作业内存的数据一致性,是` F E $ ^ T T的在多线程并发、指令重排序优化的环境中程序能如预期运转。

本文介绍了Java内存模型,以及其环绕的有序性、内存可见性以及原子性相关的常识。不得不说,关于Java内存模型,真的要深究估量能够写出一本小书,有兴趣的读者能够参阅其他材料做更深的了解。

上文中说到的valotil| b y J R Vesynchronized,是比较重要的内容,会有单独的章节。

参阅

  • 《Javi 9 ) d P o na 并发编程之美》
  • 《深入了解Java虚拟机》
  • JSR133中文

发表评论

提供最优质的资源集合

立即查看 了解详情