자바 메모리 모델 연구 노트 (A) - 기본

1 분류 동시 프로그래밍 모델

: 동시 프로그래밍에서, 우리는 두 가지 주요 질문 해결해야 스레드간에 동기화하는 방법 스레드, 2, 사이에 통신하는 방법 1 . 스레드 간의 정보 교환으로 상기 통신기구에 참조하는, 동기화는 스레드 간의 상대적인 순서 발생기구를 동작시키기위한 프로그램을 말한다.

프로그래밍 명령은, 두 개의 쓰레드 간 통신 메커니즘 : 공유 메모리, 메시지 전달 . 공유 메모리 동시성 모델에서 일반적인 상태는 작성하여 스레드의 스레드 간의 프로그램을 공유 - 암시 공공 상태 읽기 메모리를 전달하기 위해. 동시성 모델 메시징에서 국가 사이에 공통점이없는, 명시 적으로 스레드간에 명시 적 메시지를 전송하여 통신해야합니다.

동시 공유 메모리 모델에서, 동기화는 프로그래머가 명시 적으로 약간 상호 배타적 인 방식 또는 코드의 일부를 서면으로 지정해야하기 때문에, 명시 적으로 수행 스레드간에 수행 할 필요가있다. 동시성 모델의 메시징에서 메시지 때문에 메시지, 따라서 동기화가 암시 적으로 수행을 받기 전에 보내야합니다.

프로그래머가 암시 적으로 수행 스레드 사이의 통신의 작동 메커니즘을 이해하지 못하는 경우 자바 공유 메모리 모델, 자바 스레드 사이의 통신은 항상 암시 적으로 수행 동시 사용, 전체 통신 프로세스는, 프로그래머에게 완전히 투명 당신은 메모리 가시성 이해할 수없는 문제의 다양한 발생합니다.

2, JMM - 자바 메모리 모델

Java에서는, 모든 인스턴스 필드가 스태틱 필드와 배열 요소가 힙 메모리에 저장되며, 힙 메모리 영역의 스레드 사이에서 공유된다. 지역 변수, 매개 변수 및 방법은 메모리 문제의 가시성이없는, 예외 처리 매개 변수는 스레드간에 공유되지 않는 정의 및 영향은 메모리 모델을 제한되지 않습니다.

자바 스레드 간의 통신 JMM 스레드가 다른 스레드에 가시적 공유 변수에 기록 할 때 (자바 메모리 모델) 제어는 JMM 결정. 추상적 인 관점에서, JMM 스레드와 메인 메모리 사이의 추상 관계 정의 (고 에너지 경고!), 메인 메모리 (주기억 장치)에 저장된 변수 스레드간에 공유, 각 스레드는 전용 갖는다 로컬 메모리 (로컬 메모리), 로컬 메모리는 공유 변수에 스레드의 복사본의 복사에 저장된다. 참고 : JMM 로컬 메모리는 추상적 인 개념이며, 실제 아니다. 추상적 인 도식 자바 메모리 모델은 다음과 같습니다 :

지도보기에서 통신 할 수있는 스레드 A와 스레드 B 사이에, 다음과 같은 두 단계를 거쳐야합니다 :

  1. 우선, 스레드는 공유 변수 메인 메모리로 플러시 로컬 메모리를 업데이트한다;

  2. 그리고, 스레드 B는 메인 메모리 (이 경우, 공유 변수 갱신 않는다)의 공유 변수를 판독한다.

JMM 모델이 개 조항 :

  • 모든 작업이 공유 변수에 대한 스레드를 읽고 메인 메모리에서 직접 쓸 수없는, 자신의 메모리에 있어야합니다;

  • 변수 값은 메인 메모리에 의해 수행 될 필요가 사이에 다른 스레드는 스레드가 통과, 다른 스레드간에 직접 변수의 작업 메모리에 액세스 할 수 없습니다.

개략도 그것을 설명하기 :

도에 도시 된 메인 메모리 A 및 스레드 B가 스레드 공유되는 변수 X는 작업 메모리로 복사한다. 가설은, 초기 값은 X = 0이다. x의 값에 자신의 로컬 메모리에있는 스레드는 개질 X는 주 메모리로 플러시 1로 변경된다. 메인 메모리에 스레드 B가이 때, 스레드 B의 로컬 메모리에있는 x의 값은 1이되고있다 수정 값 스레드 A를 읽는다. 따라서, 스레드와의 통신을 수행하는 스레드 B.

전체에서,이 두 단계는 기본적으로 메시지 스레드를 B하기 위해 스레드 A, 그리고이 과정은 메인 메모리에 의존해야, 전체 프로세스 봐. JMM를 제공하는 메인 메모리 및 각 스레드의 로컬 메모리 사이의 상호 작용을 제어하여 메모리의 가시성 (능력이 다른 스레드에 의해 볼 수있는 공유 가변 스레드의 값을 수정하기).

따라서, 두 가지를 확인해야합니다 공유 변수의 가시성을 달성하기 위해 :

  • 스레드 후 공유 변수는 메인 메모리에 작업 메모리에서 적시에 갱신의 값을 변경;

  • 다른 스레드는 작업 메모리를 업데이트하는 메인 메모리에서 공유 변수의 최신 값으로 적시 할 수있다.

在Java语言层面支持的可见性实现原理方式有SynchronizeVolatile

3、指令重排

在执行一段程序的时候,为了性能,编译器和处理器常常会对一些指令进行重排。重排序分为三种类型:

  • 编译器优化的重排序:编译器在不改变单线程语义的前提下,可以重新安排语句的执行顺序。

  • 指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行(计算机组成原理的课程中有讲到)。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  • 内存系统的重排序:由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从java源码到最终实际执行的指令序列,分别会经历下面三种重排序:

这些重排序可能导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排都禁止)。对于处理器排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是禁止所有的处理器重排序)。

JMM属于语言级的内存模型,它确保不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序指令,为程序提供一致的内存可见性保证。

4、处理器重排序与内存屏障指令

现代的处理器使用写缓冲区来临时保存向内存中写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免处理器停顿下来等待向内存中写入数据而产生的延迟。同时,通过批处理的方式刷新写缓冲区,以及合并写缓冲区对同一内存地址的多次写,可以减少对内存总线的占用。虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!为了具体说明,请看下面示例:

Processor A Processor B
a = 1; //A1 b = 2; //B1
x = b; //A2 y = a; //B2

初始状态:a = b = 0 , 处理器允许执行后得到结果:x = y = 0

这里处理器A和处理器B可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的脏数据刷新到内存中(A3,B3)。当以这种时序执行时,程序就可以得到x = y = 0的结果。

从内存操作实际发生顺序来看,直到处理器A执行A3来刷新自己的写缓存区,写操作A1才算真执行了。虽然处理器A执行内存操作的顺序为:A1->A2,但内存操作实际发生的顺序却是:A2->A1。此时,处理器A的内存操作顺序被重排序了(处理器B的情况和处理器A一样)。

这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作重排序。

为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为下列四类:

屏障类型 指令示例 说明
LoadLoadBarriers Load1; LoadLoad; Load2 确保Load1数据的装载,之前于Load2
及所有后续装载指令的装载。
StoreStoreBarriers Store1;StoreStore;Store2 确保Store1数据对其他处理器可见(刷新到内存)
,之前于Store2及所有后续存储指令的存储。
LoadStoreBarriers Load1; LoadStore;Store2 确保Load1数据装载,之前于Store2及
所有后续的存储指令刷新到内存。
StoreLoadBarriers Store1; StoreLoad;Load2 确保Store1数据对其他处理器变得可见
(指刷新到内存),之前于Load2及所有后续装载指令的装载。
StoreLoad Barriers会使该屏障之前的所有内存访问
指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)。

总结:Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,从而让程序按我们预想的流程去执行:

  • 保证特定操作的执行顺序;

  • 影响某些数据(或则是某条指令的执行结果)的内存可见性。

5、Happens-Before规则

上面的内容讲述了重排序原则,一会是编译器重排序一会是处理器重排序,如果让程序员再去了解这些底层的实现以及具体规则,那么程序员的负担就太重了,严重影响了并发编程的效率。

因此,JMM为程序员在上层提供了happens-before规则,这样我们就可以根据规则去推论跨线程的内存可见性问题,而不用再去理解底层重排序的规则。程序员对于两个操作指令是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。

从JDK5开始,java使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second) 。

与程序员密切相关的happens-before规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作;

  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁;

  • volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读;

  • 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C;

  • 线程start()规则:主线程A启动线程B,线程B中可以看到主线程启动B之前的操作。也就是start() happens before 线程B中的操作;

  • 线程join()规则:主线程A等待子线程B完成,当子线程B执行完毕后,主线程A可以看到线程B的所有操作。也就是说,子线程B中的任意操作,happens-before join()的返回。

参考资料:

[1] 程晓明. 深入理解Java内存模型

[2] 周志明. 深入理解JVM虚拟机

[3] 程晓明,方腾飞,魏鹏. java并发编程的艺术

추천

출처www.cnblogs.com/simon-1024/p/12082269.html