휘발성 키워드에 대한 심층적 이해
1. 휘발성 및 가시성
휘발성이 가시성을 보장할 수 있다는 것은 모두 알고 있는데 어떻게 보장됩니까?
이것은 Happen-before 원칙과 관련이 있습니다. 이 원칙의 세 번째 조항은 휘발성 수정 변수의 경우 쓰기 작업이 변수의 읽기 작업보다 빨라야 한다는 것입니다. 구체적인 단계는 다음과 같습니다.
스레드 A는 공유 변수를 작업 메모리로 읽어 들이고, 스레드 B도 공유 변수를 작업 메모리로 읽습니다.
스레드 A가 공유 변수를 수정한 후 즉시 메인 메모리로 새로 고쳐집니다. 이때 스레드 B의 작업 메모리에 있는 공유 변수는 유효하지 않은 것으로 설정되며 새 값을 다시 읽어야 합니다. 메인 메모리에서. 하드웨어에 반영되어 CPU의 캐시 라인이 잘못된 상태로 설정되어 있습니다.
이것은 가시성을 보장합니다. 간단히 말해서 스레드가 휘발성 변수를 수정하고 이를 주 메모리에 새로 고치면 다른 스레드의 작업 메모리에 있는 공유 변수가 무효화되고 주 메모리에서 다시 읽어야 합니다.
2. 휘발성과 질서
우리 모두는 휘발성이 질서를 보장할 수 있다는 것을 알고 있는데, 그것이 어떻게 보장되나요?
휘발성은 질서를 보장하고 상대적으로 간단합니다. JVM과 프로세서가 휘발성 키워드로 수정된 변수에 대한 명령을 재정렬하는 것을 금지하지만 최종 결과가 다음과 같은 경우 변수 앞이나 뒤의 변수를 임의로 정렬할 수 있습니다. 변경 전 결과를 유지하세요.
기본 원리
휘발성으로 수정된 변수에는 맨 아래에 "lock:" 접두사가 붙습니다. "lock" 접두사가 붙은 명령어는 메모리 장벽과 동일하며, 이는 바로 가시성을 보장하는 핵심입니다. , 이 배리어의 주요 기능은 다음과 같습니다.
명령을 재배치할 때 배리어 앞의 코드는 배리어 뒤로 재배열될 수 없으며, 배리어 뒤의 코드는 배리어 앞으로 재배열될 수 없습니다.
메모리 배리어를 실행할 때 이전 코드가 모두 실행되었고 실행 결과가 배리어 뒤의 코드에 표시되는지 확인하세요.
작업 메모리의 변수를 강제로 주 메모리로 플러시합니다.
다른 스레드의 작업 메모리에 있는 변수는 유효하지 않은 것으로 설정되며 주 메모리에서 다시 읽어야 합니다.
3. 휘발성과 원자성
우리 모두는 휘발성이 원자성을 보장할 수 없다는 것을 알고 있는데, 왜 원자성을 보장하지 못하는 걸까요?
코드 데모:
package com.github.excellent01; import java.util.concurrent.CountDownLatch; /** * @auther plg * @date 2019/5/19 9:37 */ public class TestVolatile implements Runnable { private volatile Integer num = 0; private static CountDownLatch latch = new CountDownLatch(10); @Override public void run() { for(int i = 0; i < 1000; i++){ num++; } latch.countDown(); } public Integer getNum() { return num; } public static void main(String[] args) throws InterruptedException { TestVolatile test = new TestVolatile(); for(int i = 0; i < 10; i++){ new Thread(test).start(); } latch.await(); System.out.println(test.getNum()); } }
10개의 스레드를 시작하고 각 스레드는 공유 변수 num을 1000번 추가하고 모든 스레드의 실행이 완료되면 num의 최종 결과를 인쇄합니다.
10,000개는 거의 없습니다. 이는 휘발성이 원자성을 보장할 수 없기 때문입니다.
원인 분석:
num++ 작업은 세 단계로 구성됩니다.
메인 메모리에서 num을 작업 메모리로 읽어옵니다.
작업 메모리에서 1을 늘립니다.
추가가 완료된 후 씁니다. 주 메모리 메모리로 돌아갑니다.
이 세 단계는 모두 원자 작업이지만 함께 원자 작업은 아니며 각 단계가 실행 중에 중단될 수 있습니다.
이때 num 값이 10이라고 가정합니다. 이때 스레드 A는 자체 작업 메모리로 num을 읽습니다. 스레드 B는 자신의 작업 메모리에서 num 값이 수정되어 11이 되지만, 이때 메인 메모리에 새로 고쳐지지 않았기 때문에 스레드 A는 num 값이 변경된 것을 알지 못합니다. 앞서 언급한 대로 휘발성 변수가 수정되면 다른 스레드가 이를 즉시 알 수 있으며, 이때 먼저 다른 스레드가 공유 변수의 값을 설정해야 한다는 전제가 있습니다. 자신의 작업이 무효화됩니다. 메인 메모리에 새로 고쳐지지 않았기 때문에 A는 어리석게도 모르고 10에 1을 더했습니다. 따라서 두 스레드 모두 증가 연산을 수행했지만 최종 결과는 한 번만 추가되었습니다.
이것이 휘발성이 원자성을 보장할 수 없는 이유입니다.
휘발성의 사용 시나리오
휘발성의 특성에 따라 순서와 가시성은 보장되지만 원자성은 보장할 수 없습니다. 따라서 원자성이 필요하지 않거나 원자성이 보장된 상황에서는 휘발성을 사용할 수 있습니다.
코드 데모volatile boolean shutdownRequested public void shutdown() { shutdownRequested = true; } public void work() { while(shutdownRequested) { //do stuff } }스레드가 shutdownRequested를 수정하는 한 작업을 실행하는 스레드는 이를 즉시 확인하므로 휘발성이 추가되지 않으면 작업 메모리에서 데이터를 읽을 때마다 항상 true가 됩니다. 계속 실행됩니다. 다른 사람이 중지했다는 사실조차 몰랐습니다. 코드 데모:
package com.github.excellent; import java.util.concurrent.ThreadPoolExecutor; /** * 启动线程会被阻塞,flag 从内存读入,会存入寄存器中,下次直接从寄存器取值 * 因此值一直是false * 即使别的线程已经将值更改了,它也不知道 * 加volatile即可。也可以加锁,只要保证内存可见性即可 * @auther plg * @date 2019/5/2 22:40 */ public class Testvolatile { public static boolean flag = false; public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(()->{ for(;;) { System.out.println(flag); } }); Thread thread2 = new Thread(()->{ for(;;){ flag = true; } }); thread1.start(); Thread.sleep(1000); thread2.start(); } }실행 결과: 정말 바보같습니다. 다른 사람들이 모르고 수정했는데 여전히 false가 출력됩니다. 휘발성 물질을 추가하면 괜찮을 것입니다.
위 내용은 휘발성 키워드에 대한 깊은 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!