>  기사  >  Java  >  Java에서 휘발성을 어떻게 사용합니까?

Java에서 휘발성을 어떻게 사용합니까?

PHP中文网
PHP中文网원래의
2017-06-21 13:31:382001검색

머리말

휘발성 키워드의 역할은 여러 스레드 간의 변수 가시성을 보장하는 것임을 알고 있습니다. 이는 휘발성이 없으면 java.util.concurrent 패키지의 핵심입니다. 우리를 위해 많은 동시 수업을 사용합니다.

이 글에서는 휘발성 키워드가 어떻게 여러 스레드 사이의 변수 가시성을 보장하는지 자세히 설명합니다. 그 전에 CPU 캐시에 대한 관련 지식을 설명해야 합니다. 이 부분을 익히면 휘발성을 더 잘 이해할 수 있습니다. 휘발성 키워드를 더 좋고 더 정확하게 사용하기 위한 원칙입니다.

CPU 캐시

CPU 캐시는 주로 CPU 연산 속도와 메모리 읽기 및 쓰기 속도 사이의 충돌을 해결하는 것으로 나타납니다. 왜냐하면 CPU 연산 속도가 메모리 읽기 및 쓰기보다 빠르기 때문입니다. 속도 훨씬 빠릅니다. 예를 들면 다음과 같습니다.

  • 주 메모리 액세스에는 일반적으로 수십에서 수백 클럭 주기가 걸립니다.

  • L1 캐시에 대한 읽기 및 쓰기에는 1~2클럭만 걸립니다. 주기

  • L2 캐시에 대한 읽기 및 쓰기에는 수십 클럭 사이클만 소요됩니다.

이러한 액세스 속도 차이로 인해 CPU는 데이터가 도착할 때까지 기다리거나 데이터 쓰기를 메모리에 전송하는 데 오랜 시간을 소비하게 됩니다.

이를 기반으로 대부분의 경우 CPU는 읽기 및 쓰기를 위해 메모리에 직접 액세스하지 않습니다(CPU는 메모리에 연결된 핀이 없음). 대신 임시 버퍼인 CPU 캐시를 사용합니다. CPU와 메모리 사이에 존재하므로 용량은 메모리보다 훨씬 작지만 교환 속도는 메모리보다 훨씬 빠릅니다. 캐시에 들어 있는 데이터는 메모리에 있는 데이터 중 작은 부분이지만, 이 작은 부분은 CPU가 짧은 시간 내에 접근할 예정이므로, CPU가 많은 양의 데이터를 호출하면 읽어올 수 있습니다. 캐시를 먼저 저장하여 읽기 속도를 높입니다.

읽기 순서가 CPU와 얼마나 밀접하게 통합되어 있는지에 따라 CPU 캐시는 다음과 같이 나눌 수 있습니다.

  • 레벨 1 캐시: 간단히 말해 L1 캐시는 CPU 코어 옆에 위치하며 CPU입니다. CPU와 가장 밀접하게 통합된 캐시

  • 레벨 2 캐시: 줄여서 L2 캐시로 내부 칩과 외부 칩으로 구분됩니다. 내부 칩 L2 캐시는 기본 주파수와 동일한 속도로 실행되지만 외부 칩은 L2 캐시는 기본 주파수의 절반으로 실행됩니다

  • 레벨 3 캐시: L3 캐시는 줄여서 일부 고급 CPU에서만 사용할 수 있습니다.

각 캐시 레벨에 저장된 데이터는 모두 다음 단계 캐시입니다. 이 세 가지 캐시의 기술적인 어려움은 제조 비용이 상대적으로 감소하므로 용량도 상대적으로 증가한다는 것입니다.

CPU는 데이터를 읽으려고 할 때 먼저 첫 번째 수준 캐시에서 검색합니다. 그렇지 않으면 두 번째 수준 캐시에서 검색합니다. 세 번째 수준 캐시나 메모리에서 검색합니다. 일반적으로 각 캐시 레벨의 적중률은 약 80%입니다. 이는 전체 데이터 볼륨의 80%가 첫 번째 레벨 캐시에서 찾을 수 있고 전체 데이터 볼륨의 20%만 1레벨 캐시에서 검색하면 된다는 의미입니다. 두 번째 수준 캐시, L3 캐시 또는 메모리 내 읽기.

CPU 캐시 사용으로 인한 문제

그림을 사용하여 CPU-->CPU 캐시->메인 메모리 데이터 읽기 간의 관계를 나타냅니다.

시스템이 실행 중일 때 CPU가 계산을 수행하는 과정은 다음과 같습니다.

  1. 프로그램과 데이터가 메인 메모리에 로드됩니다.

  2. 명령어와 데이터가 CPU 캐시에 로드됩니다.

  3. CPU는 명령어를 실행하고 그 결과를 캐시에 씁니다

  4. 캐시에 있는 데이터가 메인 메모리에 다시 쓰여집니다

서버가 싱글 코어 CPU라면 이 단계는 문제를 일으키지 않지만, 서버가 멀티 코어 CPU라면 문제가 발생합니다. Intel Core i7 프로세서의 캐시 개념 모델을 살펴보세요. 예를 들어(" 컴퓨터 시스템에 대한 깊은 이해"에서 찍은 사진):

다음 상황을 상상해 보세요.

  1. Core 0은 바이트를 읽습니다. 지역성 원칙에 따르면, 인접한 바이트도 읽혀집니다. 코어 0

  2. 코어 3의 캐시로 읽어들이는 것은 위와 동일한 작업을 수행하므로 코어 0과 코어 3의 캐시는 동일한 데이터

  3. Core 0을 갖습니다. 해당 바이트를 수정하고 수정했습니다. 나중에 해당 바이트가 코어 0의 캐시에 다시 기록되었지만 정보는 주 메모리에 다시 기록되지 않았습니다.

  4. Core 3은 해당 바이트에 액세스하지 않았습니다.

이 문제를 해결하기 위해 CPU 제조업체는 다음과 같은 규칙을 만들었습니다. 한 CPU가 캐시의 바이트를 수정하면 서버의 다른 CPU에 알림이 전송됩니다. 캐시는 유효하지 않은 것으로 간주됩니다 . 따라서 위의 상황에서 코어 3은 캐시의 데이터가 유효하지 않음을 발견하고 코어 0은 즉시 데이터를 주 메모리에 다시 쓴 다음 코어 3이 데이터를 다시 읽습니다.

멀티 코어 CPU를 사용할 경우 캐시 성능이 다소 저하되는 것을 볼 수 있습니다.

Java 바이트코드를 분해하고 어셈블리 수준이 휘발성 키워드에 어떤 역할을 하는지 확인하세요

위의 이론적 기초를 바탕으로 휘발성 키워드가 어떻게 구현되는지 연구할 수 있습니다. 먼저 간단한 코드를 작성하세요.

 1 /** 2  * @author 五月的仓颉 3  */ 4 public class LazySingleton { 5  6     private static volatile LazySingleton instance = null; 7      8     public static LazySingleton getInstance() { 9         if (instance == null) {10             instance = new LazySingleton();11         }12         13         return instance;14     }15     16     public static void main(String[] args) {17         LazySingleton.getInstance();18     }19     20 }

먼저 이 코드의 .class 파일을 디컴파일하고 생성된 바이트코드를 살펴보세요.

특별한 것은 없습니다. 위 그림의 getstatic, ifnonnull, new 등과 같은 바이트코드 명령어는 결국 운영체제 수준에 해당하며 우리가 사용하는 PC와 애플리케이션 서버의 CPU 아키텍처를 위해 하나씩 명령어로 변환된다는 것을 알고 있다. 일반적으로 IA-32 아키텍처입니다. 이 아키텍처에서 사용되는 명령어 세트는 CISC(Complex Instruction Set)이며 어셈블리 언어는 이 명령어 세트의 니모닉입니다.

그럼 바이트코드 수준에서는 아무런 단서를 볼 수 없으므로, 코드를 어셈블리 명령어로 변환하여 어떤 단서를 볼 수 있는지 살펴보겠습니다. Windows에서 위 코드에 해당하는 어셈블리 코드를 보는 것은 어렵지 않습니다. (호언장담, 말하기 어렵지 않습니다. 이 문제에 대해 온갖 정보를 검색했고 거의 Linux 시스템에 가상 머신을 설치할 준비가 되어 있었습니다.) , hsdis를 방문하세요. hsdis 도구를 도구 경로에서 직접 다운로드할 수 있습니다. 다운로드 후 압축을 풀고 아래와 같이 %JAVA_HOME%jrebinserver 경로에 hsdis-amd64.dll 및 hsdis-amd64.lib 두 파일을 배치합니다.

main 함수를 실행하기 전에 다음 가상 머신 매개변수를 추가하세요.

-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*LazySingleton.getInstance

그냥 main 함수를 실행하면 됩니다.

 1 Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output 2 CompilerOracle: compileonly *LazySingleton.getInstance 3 Loaded disassembler from D:\JDK\jre\bin\server\hsdis-amd64.dll 4 Decoding compiled method 0x0000000002931150: 5 Code: 6 Argument 0 is unknown.RIP: 0x29312a0 Code size: 0x00000108 7 [Disassembling for mach='amd64'] 8 [Entry Point] 9 [Verified Entry Point]10 [Constants]11   # {method} 'getInstance' '()Lorg/xrq/test/design/singleton/LazySingleton;' in 'org/xrq/test/design/singleton/LazySingleton'12   #           [sp+0x20]  (sp of caller)13   0x00000000029312a0: mov     dword ptr [rsp+0ffffffffffffa000h],eax14   0x00000000029312a7: push    rbp15   0x00000000029312a8: sub     rsp,10h           ;*synchronization entry16                                                 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@-1 (line 13)17   0x00000000029312ac: mov     r10,7ada9e428h    ;   {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}18   0x00000000029312b6: mov     r11d,dword ptr [r10+58h]19                                                 ;*getstatic instance20                                                 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@0 (line 13)21   0x00000000029312ba: test    r11d,r11d22   0x00000000029312bd: je      29312e0h23   0x00000000029312bf: mov     r10,7ada9e428h    ;   {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}24   0x00000000029312c9: mov     r11d,dword ptr [r10+58h]25   0x00000000029312cd: mov     rax,r1126   0x00000000029312d0: shl     rax,3h            ;*getstatic instance27                                                 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@16 (line 17)28   0x00000000029312d4: add     rsp,10h29   0x00000000029312d8: pop     rbp30   0x00000000029312d9: test    dword ptr [330000h],eax  ;   {poll_return}31   0x00000000029312df: ret32   0x00000000029312e0: mov     rax,qword ptr [r15+60h]33   0x00000000029312e4: mov     r10,rax34   0x00000000029312e7: add     r10,10h35   0x00000000029312eb: cmp     r10,qword ptr [r15+70h]36   0x00000000029312ef: jnb     293135bh37   0x00000000029312f1: mov     qword ptr [r15+60h],r1038   0x00000000029312f5: prefetchnta byte ptr [r10+0c0h]39   0x00000000029312fd: mov     r11d,0e07d00b2h   ;   {oop('org/xrq/test/design/singleton/LazySingleton')}40   0x0000000002931303: mov     r10,qword ptr [r12+r11*8+0b0h]41   0x000000000293130b: mov     qword ptr [rax],r1042   0x000000000293130e: mov     dword ptr [rax+8h],0e07d00b2h43                                                 ;   {oop('org/xrq/test/design/singleton/LazySingleton')}44   0x0000000002931315: mov     dword ptr [rax+0ch],r12d45   0x0000000002931319: mov     rbp,rax           ;*new  ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)46   0x000000000293131c: mov     rdx,rbp47   0x000000000293131f: call    2907c60h          ; OopMap{rbp=Oop off=132}48                                                 ;*invokespecial <init>49                                                 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@10 (line 14)50                                                 ;   {optimized virtual_call}51   0x0000000002931324: mov     r10,rbp52   0x0000000002931327: shr     r10,3h53   0x000000000293132b: mov     r11,7ada9e428h    ;   {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}54   0x0000000002931335: mov     dword ptr [r11+58h],r10d55   0x0000000002931339: mov     r10,7ada9e428h    ;   {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}56   0x0000000002931343: shr     r10,9h57   0x0000000002931347: mov     r11d,20b2000h58   0x000000000293134d: mov     byte ptr [r11+r10],r12l59   0x0000000002931351: lock add dword ptr [rsp],0h  ;*putstatic instance60                                                 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)61   0x0000000002931356: jmp     29312bfh62   0x000000000293135b: mov     rdx,703e80590h    ;   {oop('org/xrq/test/design/singleton/LazySingleton')}63   0x0000000002931365: nop64   0x0000000002931367: call    292fbe0h          ; OopMap{off=204}65                                                 ;*new  ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)66                                                 ;   {runtime_call}67   0x000000000293136c: jmp     2931319h68   0x000000000293136e: mov     rdx,rax69   0x0000000002931371: jmp     2931376h70   0x0000000002931373: mov     rdx,rax           ;*new  ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)71   0x0000000002931376: add     rsp,10h72   0x000000000293137a: pop     rbp73   0x000000000293137b: jmp     2932b20h          ;   {runtime_call}74 [Stub Code]75   0x0000000002931380: mov     rbx,0h            ;   {no_reloc}76   0x000000000293138a: jmp     293138ah          ;   {runtime_call}77 [Exception Handler]78   0x000000000293138f: jmp     292fca0h          ;   {runtime_call}79 [Deopt Handler Code]80   0x0000000002931394: call    2931399h81   0x0000000002931399: sub     qword ptr [rsp],5h82   0x000000000293139e: jmp     2909000h          ;   {runtime_call}83   0x00000000029313a3: hlt84   0x00000000029313a4: hlt85   0x00000000029313a5: hlt86   0x00000000029313a6: hlt87   0x00000000029313a7: hlt

어셈블리 코드가 너무 길어서 CPU가 어디에서 변조했는지 모를 수도 있지만 아무 것도 어렵지 않습니다. 59행과 60행만 찾으세요.

0x0000000002931351: lock add dword ptr [rsp],0h  ;*putstatic instance; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)

之所以定位到这两行是因为这里结尾写明了line 14,line 14即volatile变量instance赋值的地方。后面的add dword ptr [rsp],0h都是正常的汇编语句,意思是将双字节的栈指针寄存器+0,这里的关键就是add前面的lock指令,后面详细分析一下lock指令的作用和为什么加上lock指令后就能保证volatile关键字的内存可见性。

 

lock指令做了什么

之前有说过IA-32架构,关于CPU架构的问题大家有兴趣的可以自己查询一下,这里查询一下IA-32手册关于lock指令的描述,没有IA-32手册的可以去这个地址下载IA-32手册下载地址,是个中文版本的手册。

我摘抄一下IA-32手册中关于lock指令作用的一些描述(因为lock指令的作用在手册中散落在各处,并不是在某一章或者某一节专门讲): 

在修改内存操作时,使用LOCK前缀去调用加锁的读-修改-写操作,这种机制用于多处理器系统中处理器之间进行可靠的通讯,具体描述如下:
(1)在Pentium和早期的IA-32处理器中,LOCK前缀会使处理器执行当前指令时产生一个LOCK#信号,这种总是引起显式总线锁定出现
(2)在Pentium4、Inter Xeon和P6系列处理器中,加锁操作是由高速缓存锁或总线锁来处理。如果内存访问有高速缓存且只影响一个单独的高速缓存行,那么操作中就会调用高速缓存锁,而系统总线和系统内存中的实际区域内不会被锁定。同时,这条总线上的其它Pentium4、Intel Xeon或者P6系列处理器就回写所有已修改的数据并使它们的高速缓存失效,以保证系统内存的一致性。如果内存访问没有高速缓存且/或它跨越了高速缓存行的边界,那么这个处理器就会产生LOCK#信号,并在锁定操作期间不会响应总线控制请求
32位IA-32处理器支持对系统内存中的某个区域进行加锁的原子操作。这些操作常用来管理共享的数据结构(如信号量、段描述符、系统段或页表),两个或多个处理器可能同时会修改这些数据结构中的同一数据域或标志。处理器使用三个相互依赖的机制来实现加锁的原子操作:1、保证原子操作2、总线加锁,使用LOCK#信号和LOCK指令前缀3、高速缓存相干性协议,确保对高速缓存中的数据结构执行原子操作(高速缓存锁)。这种机制存在于Pentium4、Intel Xeon和P6系列处理器中
IA-32处理器提供有一个LOCK#信号,会在某些关键内存操作期间被自动激活,去锁定系统总线。当这个输出信号发出的时候,来自其他处理器或总线代理的控制请求将被阻塞。软件能够通过预先在指令前添加LOCK前缀来指定需要LOCK语义的其它场合。
在Intel386、Intel486、Pentium处理器中,明确地对指令加锁会导致LOCK#信号的产生。由硬件设计人员来保证系统硬件中LOCK#信号的可用性,以控制处理器间的内存访问。
对于Pentinum4、Intel Xeon以及P6系列处理器,如果被访问的内存区域是在处理器内部进行高速缓存的,那么通常不发出LOCK#信号;相反,加锁只应用于处理器的高速缓存。
<span style="color: #000000">为显式地强制执行LOCK语义,软件可以在下列指令修改内存区域时使用LOCK前缀。当LOCK前缀被置于其它指令之前或者指令没有对内存进行写操作(也就是说目标操作数在寄存器中)时,会产生一个非法操作码异常(#UD)。
【</span><span style="color: #800080">1</span><span style="color: #000000">】位测试和修改指令(BTS、BTR、BTC)
【</span><span style="color: #800080">2</span><span style="color: #000000">】交换指令(XADD、CMPXCHG、CMPXCHG8B)
【</span><span style="color: #800080">3</span><span style="color: #000000">】自动假设有LOCK前缀的XCHG指令<br>【4】下列单操作数的算数和逻辑指令:INC、DEC、NOT、NEG<br>【5】下列双操作数的算数和逻辑指令:ADD、ADC、SUB、SBB、AND、OR、XOR<br>一个加锁的指令会保证对目标操作数所在的内存区域加锁,但是系统可能会将锁定区域解释得稍大一些。<br>软件应该使用相同的地址和操作数长度来访问信号量(用作处理器之间发送信号的共享内存)。例如,如果一个处理器使用一个字来访问信号量,其它处理器就不应该使用一个字节来访问这个信号量。<br>总线锁的完整性不收内存区域对齐的影响。加锁语义会一直持续,以满足更新整个操作数所需的总线周期个数。但是,建议加锁访问应该对齐在它们的自然边界上,以提升系统性能:<br>【1】任何8位访问的边界(加锁或不加锁)<br>【2】锁定的字访问的16位边界<br>【3】锁定的双字访问的32位边界<br>【4】锁定的四字访问的64位边界<br>对所有其它的内存操作和所有可见的外部事件来说,加锁的操作都是原子的。所有取指令和页表操作能够越过加锁的指令。加锁的指令可用于同步一个处理器写数据而另一个处理器读数据的操作。</span>
IA-32架构提供了几种机制用来强化或弱化内存排序模型,以处理特殊的编程情形。这些机制包括:
【1】I/O指令、加锁指令、LOCK前缀以及串行化指令等,强制在处理器上进行较强的排序
【2】SFENCE指令(在Pentium III中引入)和LFENCE指令、MFENCE指令(在Pentium4和Intel Xeon处理器中引入)提供了某些特殊类型内存操作的排序和串行化功能
...(这里还有两条就不写了)
这些机制可以通过下面的方式使用。
总线上的内存映射设备和其它I/O设备通常对向它们缓冲区写操作的顺序很敏感,I/O指令(IN指令和OUT指令)以下面的方式对这种访问执行强写操作的排序。在执行了一条I/O指令之前,处理器等待之前的所有指令执行完毕以及所有的缓冲区都被都被写入了内存。只有取指令和页表查询能够越过I/O指令,后续指令要等到I/O指令执行完毕才开始执行。

反复思考IA-32手册对lock指令作用的这几段描述,可以得出lock指令的几个作用:

  1. 锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存

  2. lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据

  3. 不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序

(1)中写了由于效率问题,实际后来的处理器都采用锁缓存来替代锁总线,这种场景下多缓存的数据一致是通过缓存一致性协议来保证的,我们来看一下什么是缓存一致性协议。 

 

缓存一致性协议

讲缓存一致性之前,先说一下缓存行的概念:

  • 缓存是分段(line)的,一个段对应一块存储空间,我们称之为缓存行,它是CPU缓存中可分配的最小存储单元,大小32字节、64字节、128字节不等,这与CPU架构有关。当CPU看到一条读取内存的指令时,它会把内存地址传递给一级数据缓存,一级数据缓存会检查它是否有这个内存地址对应的缓存段,如果没有就把整个缓存段从内存(或更高一级的缓存)中加载进来。注意,这里说的是一次加载整个缓存段,这就是上面提过的局部性原理

上面说了,LOCK#会锁总线,实际上这不现实,因为锁总线效率太低了。因此最好能做到:使用多组缓存,但是它们的行为看起来只有一组缓存那样。缓存一致性协议就是为了做到这一点而设计的,就像名称所暗示的那样,这类协议就是要使多组缓存的内容保持一致

缓存一致性协议有多种,但是日常处理的大多数计算机设备都属于"嗅探(snooping)"协议,它的基本思想是:

<span style="color: #000000">所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线:缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个CPU缓存可以读写内存)。<br>CPU缓存不仅仅在做内存传输的时候才与总线打交道,而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。所以当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器一写内存,其它处理器马上知道这块内存在它们的缓存段中已失效。</span>

MESI 프로토콜은 현재 가장 주류인 캐시 일관성 프로토콜입니다. MESI 프로토콜에서 각 캐시 라인에는 2비트로 표현될 수 있는 4가지 상태가 있습니다. M 상태에는 이미 유효하지 않은/언로드된 캐시 세그먼트, 깨끗한 캐시 세그먼트 및 더티 캐시 세그먼트라는 해당 개념이 있습니다. 따라서 여기서 새로운 지식 포인트는 독점 액세스를 나타내는 E 상태뿐입니다. 이 상태는 "특정 메모리 부분을 수정하기 전에 다른 프로세서에 알려야 합니다"라는 문제를 해결합니다. E 또는 M 상태에서는 프로세서가 이를 쓸 수 있습니다. 이는 이 두 상태에서만 프로세서가 이 캐시 라인을 독점적으로 점유한다는 것을 의미합니다. 프로세서가 특정 캐시 라인에 쓰기를 원할 때 독점 권한이 없으면 먼저 버스에 "독점 권한을 원합니다" 요청을 보내야 합니다. 동일한 캐시 세그먼트가 유효하지 않습니다

(있는 경우). 독점 권한을 얻은 후에만 프로세서가 데이터 수정을 시작할 수 있습니다. 이 시점에서 프로세서는 내 캐시에 이 캐시 라인의 복사본이 하나만 있다는 것을 알게 되므로 충돌이 발생하지 않습니다.

반면에 다른 프로세서가 이 캐시 라인을 읽으려는 경우(항상 버스를 스니핑하고 있으므로 즉시 알 수 있음) 독점 또는 수정된 캐시 라인을 먼저 "공유" 상태로 되돌려야 합니다. 수정된 캐시 라인인 경우 먼저 콘텐츠를 메모리에 다시 써야 합니다.

잠금 명령을 통해 휘발성 변수 읽기 및 쓰기 검토

위의 잠금 설명을 통해 휘발성 키워드의 구현 원리가 한눈에 명확해야 한다고 생각합니다. 먼저 사진을 보세요.

작업 메모리는 실제로 CPU 레지스터와 캐시의 추상화입니다. 또는 각 스레드의 작업 메모리도 간단히 CPU 레지스터와 캐시로 이해될 수 있습니다.

그런 다음 두 스레드 Thread-A와 Threab-B를 작성하여 동시에 주 메모리의 휘발성 변수 i를 작동할 때 Thread-A는 변수 i를 작성한 다음:

Thread-A LOCK #LOCK 명령에 의해 발행됨

# 명령은 버스를 잠그거나 캐시 라인을 잠그는 동시에 Thread-B 캐시의 캐시 라인 내용을 무효화합니다.

  • Thread-A 쓰기 수정된 i

  • Thread-B는 변수 i를 읽은 후 다음과 같이 최신 내용을 주 메모리로 되돌립니다.

  • Thread-B는 해당 주소에 해당하는 캐시 라인이 잠겨 있음을 발견하고 잠금을 기다립니다. 캐시 일관성 프로토콜이 이를 보장합니다. 최신 값을 읽습니다

휘발성 키워드를 읽는 것과 일반 변수를 읽는 것 사이에는 기본적으로 차이가 없음을 알 수 있습니다. 주요 차이점은 쓰기 작업에 있습니다. 변수의.

  • Postscript

저는 개인적으로 이전에도 휘발성 키워드의 역할에 대해 다소 혼란스러운 오해를 갖고 있었습니다. 휘발성 키워드의 역할을 깊이 이해하고 나니 휘발성에 대한 이해가 훨씬 깊어진 것 같습니다. 이 글을 읽으신다면, 기꺼이 생각하고 공부하신다면 저와 같은 갑작스런 깨달음과 깨달음의 느낌을 가지실 것이라 믿습니다^_^

References

"IA -32 아키텍처 소프트웨어 개발 인력 매뉴얼 3권: 시스템 프로그래밍 가이드"

"Java의 동시 프로그래밍 기술""Java Virtual Machine에 대한 심층적인 이해: JVM 고급 기능 및 모범 사례"

PrintAssembly 휘발성 어셈블리 코드 보기 작게 기억하기

캐시 일관성 시작하기

높은 동시성에 대해 이야기하기 (34) Java 메모리 모델 (2) CPU 캐시의 작동 원리 이해하기

위 내용은 Java에서 휘발성을 어떻게 사용합니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.