메모리 모델 관련 개념
동시 프로그래밍의 세 가지 개념
Java 메모리 모델
휘발성키워드 심층 분석
휘발성 키워드 사용 시나리오
캐시 일관성 문제. 이러한 유형의 변수를 일반적으로 공유 변수라고 합니다.
즉, 변수가 여러 CPU에 캐시되는 경우(일반적으로) 멀티스레드에서만 발생합니다. 프로그래밍), 캐시 불일치 문제가 있을 수 있습니다.
캐시 불일치 문제를 해결하려면 일반적으로 두 가지 해결 방법이 있습니다.
버스에 LOCK#을 추가하는 방법
캐시 일관성 프로토콜을 통해
이 두 가지 방법은 하드웨어 수준에서 제공되는 방법
위의 방법 1은 잠금 기간 동안 다른 CPU가 메모리에 액세스할 수 없어 비효율성을 초래하므로 문제가 발생합니다.
가장 유명한 캐시 일관성 프로토콜은 Intel의 MESI 프로토콜입니다. 각 캐시에 사용되는 공유 변수는 일관됩니다. 핵심 아이디어는 CPU가 데이터를 쓸 때 작동 변수가 공유 변수인 것으로 확인되면 다른 CPU에도 해당 변수의 복사본이 있다는 것입니다. CPU는 변수의 캐시 라인을 무효화하도록 신호를 보냅니다. 따라서 다른 CPU가 이 변수를 읽어야 할 때 자신의 캐시에 변수를 캐싱하는 캐시 라인이 유효하지 않다는 것을 알게 됩니다. 메모리에서 다시 읽기
동시 프로그래밍에서는 일반적으로 원자성 문제, 가시성 문제, 순서 문제
원자성: 하나의 작업 또는 여러 작업이 모두 실행되고 실행되는 프로세스입니다. 어떤 요인에도 중단되지 않습니다.
가시성은 여러 스레드가 동일한 변수에 액세스할 때 한 스레드가 변수 값을 수정하면 다른 스레드가 수정된 값을 즉시 볼 수 있음을 의미합니다.
3.3 질서성 순서성: 즉, 프로그램 실행 순서는 코드 순서로 보면 명령문 1이 명령문 2보다 먼저 실행됩니다. 따라서 JVM이 실제로 이를 실행할 때 코드에서는 명령문 1이 명령문 2보다 먼저 실행되도록 보장합니까? 반드시 그런 것은 아닙니다. 여기서 정렬(명령어 재정렬)이 발생할 수 있는 이유는 무엇입니까? 다음은 명령어 재정렬이 무엇인지 설명합니다. 일반적으로 프로그램 작업의 효율성을 높이기 위해 프로세서는 입력 코드를 최적화할 수 있습니다. 이는 프로그램의 각 명령문의 실행 순서를 보장하지 않습니다. 코드와 동일합니다. 순서는 일관되지만 프로그램의 최종 실행 결과는 코드의 순차적 실행 결과와 일치합니다. 명령어 재정렬은 단일 스레드 실행에는 영향을 미치지 않지만 스레드 동시 실행의 정확성에는 영향을 미칩니다. 즉, 동시성 프로그램이 올바르게 실행되기 위해서는 원자성, 가시성, 질서가 보장되어야 합니다. 그 중 하나가 보장되지 않는 한 프로그램이 잘못 실행될 수 있습니다. 4. Java 메모리 모델 Java Virtual Machine 사양에서는 다양한 보호를 위해 Java 메모리 모델(Java Memoryx++; //문장 3
사실 문 1만 원자 연산이고, 나머지 세 문은 원자 연산이 아닙니다.
즉, 단순한 읽기와 할당(그리고 변수에 숫자를 할당해야 하며, 변수 간의 상호 할당은 원자적 연산이 아님)만이 원자적 연산입니다.
위에서 볼 수 있듯이 Java 메모리 모델은 기본 읽기 및 할당이 원자성 작업인 것만 보장합니다. 더 넓은 범위의 작업에 대한 원자성을 달성하려면 동기화 및 잠금을 통해 이를 달성할 수 있습니다. .
가시성을 위해 Java는 가시성을 보장하기 위해 휘발성 키워드를 제공합니다.
공유 변수가 휘발성으로 수정되면 수정된 값이 즉시 메인 메모리에 업데이트됩니다. 다른 스레드가 이를 읽어야 할 경우 메모리에서 새 값을 읽습니다.
일반 공유 변수는 가시성을 보장할 수 없습니다. 일반 공유 변수가 수정된 후, 다른 스레드가 이를 읽을 때 메모리가 여전히 원래의 이전 값에 기록될 수 있는지 알 수 없기 때문입니다. 이므로 가시성이 보장되지 않습니다.
또한, 동기화 및 잠금을 통해 가시성도 보장할 수 있습니다. 동기화 및 잠금은 하나의 스레드만 동시에 잠금을 획득한 후 동기화 코드를 실행하도록 보장할 수 있으며, 변수의 수정은 다음과 같습니다. 잠금을 해제하기 전에 메인 메모리로 플러시됩니다. 따라서 가시성이 보장됩니다.
Java 메모리 모델에서는 컴파일러와 프로세서가 명령어를 재정렬할 수 있지만 재정렬 프로세스는 단일 스레드 프로그램의 실행에는 영향을 미치지 않지만 정확성에는 영향을 미칩니다. 멀티스레드 동시 실행.
Java에서는 휘발성 키워드를 사용하여 특정 "질서성"을 보장할 수 있습니다(명령어 재정렬을 금지할 수 있음). 또한 동기화 및 잠금을 통해 질서를 보장할 수 있습니다. 당연히 동기화 및 잠금은 하나의 스레드가 매 순간 동기화 코드를 실행하도록 보장합니다. 이는 스레드가 동기화 코드를 순차적으로 실행하도록 하는 것과 동일하므로 자연스럽게 질서가 보장됩니다.
또한 Java 메모리 모델에는 선천적인 "질서성", 즉 아무런 수단 없이도 보장될 수 있는 질서성이 있습니다. 이를 흔히 사전 발생 원칙이라고 합니다. 두 작업의 실행 순서를 사전 발생 원칙에서 추론할 수 없는 경우 순서가 보장되지 않으며 가상 머신이 마음대로 순서를 변경할 수 있습니다.
다음은 사전 발생 원칙에 대한 자세한 소개입니다.
프로그램 순서 규칙: 스레드 내에서는 코드 순서에 따라 앞에 쓰는 작업입니다.
이후 작성된 작업 이전에 발생 잠금 규칙: 동일한 잠금 이후 동일한 잠금 작업 이전에 잠금 해제 작업이 발생
휘발성 변수 규칙: 변수에 대한 쓰기 작업이 이 변수에 대한 후속 읽기 작업보다 먼저 발생합니다.
전파 규칙: 작업 A가 작업 B보다 먼저 발생하고 작업 B가 발생하는 경우 C 작업 이전에 A 작업이 C 작업보다 먼저 발생한다고 결론을 내릴 수 있습니다
스레드 시작 규칙: Thread 객체의 start() 메서드가 여기에서 먼저 발생합니다. 스레드의 모든 작업
스레드 중단 규칙: 중단된 스레드의 코드가 인터럽트 이벤트 발생을 감지하면 스레드 Interrupt() 메서드 호출이 먼저 발생합니다.
스레드 종료 규칙: 스레드의 모든 작업은 스레드가 종료될 때 먼저 발생합니다. Thread.join() 메서드와 Thread.isAlive()의 반환 값을 통해 스레드가 종료되었음을 감지할 수 있습니다.
네 번째 규칙은 실제로 사전 발생 원칙의 전이적 특성을 반영합니다.
일단 공유변수(멤버변수) 클래스의 클래스 정적 멤버 변수)가 휘발성으로 수정된 후 두 가지 수준의 의미를 갖습니다.
는 이 변수에 대해 서로 다른 스레드, 즉 하나의 스레드가 작동할 때 가시성을 보장합니다. 특정 변수 값을 수정하면 새 값이 다른 스레드에 즉시 표시됩니다.
명령어 재정렬은 금지됩니다.
가시성에 관해 먼저 코드를 살펴보겠습니다. 스레드 1이 먼저 실행되고 스레드 2가 나중에 실행되는 경우:
//线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true;
이 코드는 매우 일반적입니다. 코드 조각에는 많은 사람들이 스레드를 중단할 때 이 표시 방법을 사용할 수 있습니다. 하지만 실제로 이 코드가 완전히 올바르게 실행될까요? 즉, 스레드가 중단됩니까? 반드시 그런 것은 아니지만 대부분의 경우 이 코드는 스레드를 중단할 수 있지만 스레드를 중단할 수 없게 만들 수도 있습니다(이런 가능성은 매우 적지만 일단 이런 일이 발생하면 무한 루프가 발생합니다).
다음은 이 코드로 인해 스레드가 중단되지 않는 이유를 설명합니다. 앞에서 설명했듯이 각 스레드는 실행 중에 자체 작업 메모리를 가지므로 스레드 1이 실행 중일 때 중지 변수의 값을 복사하여 자체 작업 메모리에 넣습니다.
그런 다음 스레드 2가 중지 변수의 값을 변경했지만 이를 주 메모리에 쓸 시간이 되기 전에 스레드 2는 다른 작업을 수행하도록 전환하고 스레드 1은 중지 변수의 값을 알지 못합니다. 스레드 2에 의해 변수가 지정되므로 주기가 계속됩니다.
그러나 휘발성 수정을 사용한 후에는 달라집니다.
첫 번째: 휘발성 키워드를 사용하면 수정된 값이 즉시 메인 메모리에 기록됩니다. >
아아아아
모두들 이 프로그램의 결과물에 대해 생각하시나요? 어쩌면 어떤 친구들은 그것이 10,000이라고 생각할 수도 있습니다. 그런데 실제로 실행해 보면 매번 결과가 일관되지 않고, 항상 10,000 미만의 숫자임을 알 수 있습니다. 여기서 오해가 있습니다. 휘발성 키워드는 가시성을 보장할 수 있지만 위 프로그램의 오류는 원자성을 보장하지 못한다는 것입니다. 가시성은 매번 최신 값을 읽는 것만 보장할 수 있지만 휘발성은 변수 작업의 원자성을 보장할 수 없습니다. 앞서 언급했듯이 자동 증가 작업은 원자성이 아닙니다. 여기에는 변수의 원래 값을 읽고 1을 더하고 작업 메모리에 쓰는 작업이 포함됩니다. 즉, 자동 증가 연산의 세 가지 하위 연산이 별도로 실행될 수 있으며 이로 인해 다음과 같은 상황이 발생할 수 있습니다. 특정 순간에 변수 inc의 값이 10인 경우. 스레드 1은 변수에 대해 자동 증가 작업을 수행합니다. 스레드 1은 먼저 변수 inc의 원래 값을 읽은 다음 스레드 1이 차단됩니다. 스레드 2는 자동 증가 작업을 수행합니다. 스레드 2는 변수 inc의 원래 값도 읽습니다. 스레드 1은 변수 inc만 읽고 변수를 수정하지 않으므로 스레드 2의 작업 메모리에 있는 캐시된 변수 inc의 캐시 라인은 변경되지 않습니다. 따라서 스레드 2는 직접 주 메모리로 이동하여 inc 값을 읽고 inc 값이 10임을 확인한 다음 1을 더하고 11을 작업 메모리에 쓴 다음 마지막으로 주 메모리에 씁니다. . 그런 다음 스레드 1은 1을 더합니다. inc 값을 읽었으므로 이때 스레드 1의 작업 메모리에 있는 inc 값은 여전히 10이므로 스레드 1은 inc에 1을 더합니다. 마지막 inc의 값은 11이고 11이 작업 메모리에 기록되고 마지막으로 주 메모리에 기록됩니다. 그런 다음 두 스레드가 각각 자동 증가 작업을 수행한 후 inc는 1씩만 증가합니다. 이렇게 설명하니 궁금한 친구들이 있을 수도 있겠네요. 아니요. 변수를 휘발성 변수로 수정하면 캐시 라인이 무효화된다는 보장은 없나요? 그러면 다른 스레드가 새 값을 읽습니다. 예, 맞습니다. 이는 위의 발생 전 규칙에 있는 휘발성 변수 규칙이지만 스레드 1이 변수를 읽고 차단된 후에는 inc 값이 수정되지 않는다는 점에 유의해야 합니다. 그런 다음 휘발성은 스레드 2가 메모리에서 변수 inc의 값을 읽는 것을 보장할 수 있지만 스레드 1은 이를 수정하지 않았으므로 스레드 2는 수정된 값을 전혀 볼 수 없습니다.根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
把上面的代码改成以下任何一种都可以达到效果:
采用synchronized:
public class Test { public int inc = 0; public synchronized void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
采用Lock:
public class Test { public int inc = 0; Lock lock = new ReentrantLock(); public void increase() { lock.lock(); try { inc++; } finally{ lock.unlock(); } } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
采用AtomicInteger:
public class Test { public AtomicInteger inc = new AtomicInteger(); public void increase() { inc.getAndIncrement(); } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。
volatile能在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
举个例子:
//x、y为非volatile变量 //flag为volatile变量 x = 2; //语句1 y = 0; //语句2 flag = true; //语句3 x = 4; //语句4 y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
这里探讨一下volatile到底如何保证可见性和禁止指令重排序的。
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
它会强制将对缓存的修改操作立即写入主存;
如果是写操作,它会导致其他CPU中对应的缓存行无效。
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
对变量的写操作不依赖于当前值(比如++操作,上面有例子)
该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
状态标记量
volatile boolean flag = false; while(!flag){ doSomething(); } public void setFlag() { flag = true; }
volatile boolean inited = false; //线程1: context = loadContext(); inited = true; //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } }
至于为何需要这么写请参考:
위 내용은 루트에서 Java 휘발성 키워드 구현의 샘플 코드 분석(그림)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!