JMM은 Java 메모리 모델입니다. 하드웨어 제조업체와 운영 체제에 따라 메모리 액세스에 특정 차이가 있기 때문에 동일한 코드가 다른 시스템에서 실행될 때 다양한 문제가 발생합니다. 따라서 JMM(Java 메모리 모델)은 다양한 하드웨어 및 운영 체제의 메모리 액세스 차이를 보호하여 다양한 플랫폼에서 Java 프로그램에 대한 일관된 동시성 효과를 달성합니다.
Java 메모리 모델은 인스턴스 변수와 정적 변수를 포함한 모든 변수가 메인 메모리에 저장되지만 로컬 변수와 메소드 매개변수는 포함되지 않는다고 규정합니다. 각 스레드에는 자체 작업 메모리가 있습니다. 스레드의 작업 메모리는 스레드에서 사용하는 변수를 저장하며 변수에 대한 스레드의 작업은 모두 작업 메모리에서 수행됩니다. 스레드는 주 메모리에서 변수를 직접 읽거나 쓸 수 없습니다.
서로 다른 스레드는 서로의 작업 메모리에 있는 변수에 액세스할 수 없습니다. 스레드 간의 변수값 전달은 메인 메모리를 통해 완료되어야 합니다.
각 스레드의 작업 메모리는 독립적입니다. 스레드 작업 데이터는 작업 메모리에서만 수행된 다음 기본 메모리로 다시 플러시될 수 있습니다. 이는 Java 메모리 모델에 정의된 대로 스레드가 작동하는 기본 방식입니다.
여기 있는 일부 사람들은 Java 메모리 모델을 Java 메모리 구조로 오해하고 힙, 스택, GC 가비지 수집에 대해 답변할 것이며 결국 면접관이 묻고 싶은 질문과 거리가 멀다는 점을 알려드립니다. 실제로 Java 메모리 모델에 관해 질문을 받으면 일반적으로 멀티스레딩 및 Java 동시성과 관련된 질문을 하고 싶어합니다.
이것은 간단합니다. 전체 Java 메모리 모델은 실제로 세 가지 특성을 중심으로 구축됩니다. 그것은 원자성, 가시성, 질서입니다. 이 세 가지 특성은 전체 Java 동시성의 기초라고 할 수 있습니다.
원자성은 작업이 분할 불가능하고 중단할 수 없으며 스레드가 실행 중에 다른 스레드의 방해를 받지 않음을 의미합니다.
면접관은 펜을 들고 코드를 작성했습니다. 다음 코드는 원자성을 보장할 수 있나요?
int i = 2; int j = i; i++; i = i + 1;
첫 번째 문장은 기본 유형 할당 연산으로, 원자 연산이어야 합니다.
두 번째 문장은 i의 값을 먼저 읽은 다음 이를 j에 할당합니다. 이 2단계 연산은 원자성을 보장할 수 없습니다.
세 번째와 네 번째 문장은 실제로 동일합니다. 먼저 i 값을 읽은 다음 +1하고 마지막으로 i에 값을 할당합니다. 이는 3단계 작업이므로 원자성을 보장할 수 없습니다.
JMM은 코드 블록의 원자성을 보장하려는 경우 동기화 키워드인 monitorenter 및 moniterexit라는 두 가지 바이트코드 명령어를 제공합니다. 따라서 동기화된 블록 간의 작업은 원자적입니다.
가시성은 한 스레드가 공유 변수의 값을 수정하면 다른 스레드가 해당 값이 수정되었음을 즉시 알 수 있음을 의미합니다. Java는 가시성을 제공하기 위해 휘발성 키워드를 사용합니다. 변수가 휘발성으로 수정되면 해당 변수는 수정된 후 즉시 주 메모리로 플러시됩니다. 다른 스레드가 변수를 읽어야 할 경우 주 메모리에서 새 값을 읽습니다. 일반 변수는 이를 보장할 수 없습니다.
휘발성 키워드 외에도 최종 및 동기화도 가시성을 얻을 수 있습니다.
동기화의 원칙은 실행 후 잠금 해제에 들어가기 전에 공유 변수를 메인 메모리에 동기화해야 한다는 것입니다.
최종 수정 필드는 초기화가 완료되면 개체가 이스케이프되지 않으면 다른 스레드에 표시됩니다(즉, 초기화가 완료된 후 다른 스레드에서 개체를 사용할 수 있음을 의미).
Java에서는 동기화 또는 휘발성을 사용하여 여러 스레드 간의 작업 순서를 보장할 수 있습니다. 구현 원칙에는 몇 가지 차이점이 있습니다.
휘발성 키워드는 질서를 보장하기 위해 메모리 장벽을 사용하여 명령 재정렬을 금지합니다.
동기화의 원칙은 스레드가 잠긴 후 다른 스레드가 다시 잠기기 전에 잠금을 해제해야 동기화로 래핑된 코드 블록이 여러 스레드 간에 순차적으로 실행된다는 것입니다.
8가지 유형의 메모리 상호 작용 작업이 있습니다.
lock(잠금)은 주 메모리의 변수에 대해 작동하고 변수를 스레드 독점으로 표시합니다.
read(읽기)는 주 메모리의 변수에 작용하고 다음 로드 작업에 사용하기 위해 주 메모리에서 스레드의 작업 메모리로 변수 값을 전송합니다.
load(로딩)는 작업 메모리의 변수에 작용하며 읽기 작업의 주 메모리 변수를 작업 메모리의 변수 복사본에 넣습니다.
use(사용)는 작업 메모리의 변수에 작용하고 작업 메모리의 변수를 실행 엔진으로 전송합니다. 이 작업은 가상 머신이 값을 사용해야 하는 바이트코드 명령을 만날 때마다 수행됩니다. 변수.
작업 메모리의 변수에 작용하는 할당(할당)은 가상 머신이 할당하는 바이트코드 명령어를 만날 때마다 실행 엔진에서 받은 값을 작업 메모리의 변수 복사본에 할당합니다. 값을 변수로 이 작업이 수행됩니다.
작업 메모리의 변수에 작용하는 저장소(저장소)는 후속 쓰기 사용을 위해 작업 메모리에서 주 메모리로 변수 값을 전송합니다.
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
我再补充一下JMM对8种内存交互操作制定的规则吧:
不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
不允许线程将没有assign的数据从工作内存同步到主内存。
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。
很多并发编程都使用了volatile关键字,主要的作用包括两点:
保证线程间变量的可见性。
禁止CPU进行指令重排序。
volatile修饰的变量,当一个线程改变了该变量的值,其他线程是立即可见的。普通变量则需要重新读取才能获得最新值。
volatile保证可见性的流程大概就是这个一个过程:
先说结论吧,volatile不能一定能保证线程安全。
怎么证明呢,我们看下面一段代码的运行结果就知道了:
public class VolatileTest extends Thread { private static volatile int count = 0; public static void main(String[] args) throws Exception { Vector<Thread> threads = new Vector<>(); for (int i = 0; i < 100; i++) { VolatileTest thread = new VolatileTest(); threads.add(thread); thread.start(); } //等待子线程全部完成 for (Thread thread : threads) { thread.join(); } //输出结果,正确结果应该是1000,实际却是984 System.out.println(count);//984 } @Override public void run() { for (int i = 0; i < 10; i++) { try { //休眠500毫秒 Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } count++; } } }
为什么volatile不能保证线程安全?
很简单呀,可见性不能保证操作的原子性,前面说过了count++不是原子性操作,会当做三步,先读取count的值,然后+1,最后赋值回去count变量。需要保证线程安全的话,需要使用synchronized关键字或者lock锁,给count++这段代码上锁:
private static synchronized void add() { count++; }
首先要讲一下as-if-serial语义,不管怎么重排序,(单线程)程序的执行结果不能被改变。
为了使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率,只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。
重排序的种类分为三种,分别是:编译器重排序,指令级并行的重排序,内存系统重排序。整个过程如下所示:
指令重排序在单线程是没有问题的,不会影响执行结果,而且还提高了性能。但是在多线程的环境下就不能保证一定不会影响执行结果了。
所以在多线程环境下,就需要禁止指令重排序。
volatile关键字禁止指令重排序有两层意思:
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
下面举个例子:
private static int a;//非volatile修饰变量 private static int b;//非volatile修饰变量 private static volatile int k;//volatile修饰变量 private void hello() { a = 1; //语句1 b = 2; //语句2 k = 3; //语句3 a = 4; //语句4 b = 5; //语句5 //... }
变量a,b是非volatile修饰的变量,k则使用volatile修饰。所以语句3不能放在语句1、2前,也不能放在语句4、5后。但是语句1、2的顺序是不能保证的,同理,语句4、5也不能保证顺序。
并且,执行到语句3的时候,语句1,2是肯定执行完毕的,而且语句1,2的执行结果对于语句3,4,5是可见的。
首先要讲一下内存屏障,内存屏障可以分为以下几类:
LoadLoad 장벽: Load1, LoadLoad, Load2와 같은 명령문용. Load2에서 읽을 데이터와 후속 읽기 작업에 액세스하기 전에 Load1에서 읽을 데이터를 읽었다는 것이 보장됩니다.
StoreStore 장벽: Store2 및 후속 쓰기 작업이 실행되기 전의 Store1, StoreStore, Store2 명령문의 경우 Store1의 쓰기 작업이 다른 프로세서에 표시된다는 것이 보장됩니다.
LoadStore 장벽: Load1, LoadStore 및 Store2와 같은 문의 경우 Store2 및 후속 쓰기 작업이 플러시되기 전에 Load1에서 읽을 데이터를 읽어야 합니다.
StoreLoad 장벽: Store1, StoreLoad 및 Load2와 같은 문의 경우 Load2 및 모든 후속 읽기 작업이 실행되기 전에 Store1에 대한 쓰기가 모든 프로세서에 표시되도록 보장됩니다.
각 휘발성 읽기 작업 후에 LoadLoad 장벽을 삽입하고 읽기 작업 후에 LoadStore 장벽을 삽입합니다.
각 휘발성 쓰기 작업 앞에 StoreStore 장벽을 삽입하고 뒷면에 SotreLoad 장벽을 삽입합니다.
위 내용은 Java의 JMM 높은 동시성 프로그래밍 예제 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!