Heim  >  Artikel  >  Java  >  Analyse von JMM-Programmierbeispielen mit hoher Parallelität in Java

Analyse von JMM-Programmierbeispielen mit hoher Parallelität in Java

WBOY
WBOYnach vorne
2023-05-02 18:52:07687Durchsuche

1. Was ist JMM

JMM ist das Java-Speichermodell (Java-Speichermodell). Da es bei verschiedenen Hardwareherstellern und Betriebssystemen gewisse Unterschiede beim Speicherzugriff gibt, treten verschiedene Probleme auf, wenn derselbe Code auf verschiedenen Systemen ausgeführt wird. Daher schützt das Java Memory Model (JMM) die Speicherzugriffsunterschiede verschiedener Hardware und Betriebssysteme, um konsistente Parallelitätseffekte für Java-Programme auf verschiedenen Plattformen zu erzielen.

Das Java-Speichermodell schreibt vor, dass alle Variablen im Hauptspeicher gespeichert werden, einschließlich Instanzvariablen und statischen Variablen, jedoch keine lokalen Variablen und Methodenparameter. Jeder Thread verfügt über einen eigenen Arbeitsspeicher. Der Arbeitsspeicher des Threads speichert die vom Thread verwendeten Variablen und eine Kopie des Hauptspeichers. Alle Operationen des Threads an Variablen werden im Arbeitsspeicher ausgeführt. Threads können Variablen im Hauptspeicher nicht direkt lesen oder schreiben.

Verschiedene Threads können nicht auf Variablen im Arbeitsspeicher des anderen zugreifen. Die Übertragung variabler Werte zwischen Threads muss über den Hauptspeicher erfolgen.

Analyse von JMM-Programmierbeispielen mit hoher Parallelität in Java

Die Thread-Operationsdaten können nur im Arbeitsspeicher ausgeführt und dann zurück in den Hauptspeicher geleert werden. Dies ist die grundlegende Funktionsweise von Threads, wie sie im Java-Speichermodell definiert ist.

Eine herzliche Erinnerung: Einige Leute hier werden das Java-Speichermodell als Java-Speicherstruktur missverstehen und dann mit Heap, Stack und GC-Garbage Collection antworten, und schließlich ist es weit von der Frage entfernt, die der Interviewer stellen möchte fragen. Wenn sie nach dem Java-Speichermodell gefragt werden, möchten sie im Allgemeinen Fragen zu Multithreading und Java-Parallelität stellen.

2. Was definiert JMM?

Das gesamte Java-Speichermodell basiert eigentlich auf drei Merkmalen. Sie sind: Atomizität, Sichtbarkeit und Ordnung. Man kann sagen, dass diese drei Merkmale die Grundlage der gesamten Java-Parallelität bilden.

Atomizität

Atomizität bedeutet, dass eine Operation unteilbar und ununterbrochen ist und ein Thread während der Ausführung nicht durch andere Threads gestört wird.

Der Interviewer nahm einen Stift und schrieb einen Code. Können die folgenden Codezeilen Atomizität garantieren?

int i = 2;
int j = i;
i++;
i = i + 1;

Der erste Satz ist eine grundlegende Typzuweisungsoperation, bei der es sich um eine atomare Operation handeln muss.

Der zweite Satz liest zuerst den Wert von i und weist ihn dann j zu. Diese zweistufige Operation kann keine Atomizität garantieren.

Der dritte und vierte Satz sind tatsächlich gleichwertig. Lesen Sie zuerst den Wert von i, dann +1 und weisen Sie ihn schließlich zu. Es handelt sich um eine dreistufige Operation, die keine Atomizität garantieren kann.

JMM kann nur die grundlegende Atomizität garantieren. Wenn Sie die Atomizität eines Codeblocks sicherstellen möchten, stellt es zwei Bytecode-Anweisungen bereit, Monitorenter und Monitorexit, die das synchronisierte Schlüsselwort sind. Daher sind Operationen zwischen synchronisierten Blöcken atomar.

Sichtbarkeit

Sichtbarkeit bedeutet, dass andere Threads sofort erkennen können, dass der Wert einer gemeinsam genutzten Variablen geändert wurde, wenn ein Thread ihn ändert. Java verwendet das Schlüsselwort volatile, um Sichtbarkeit bereitzustellen. Wenn eine Variable flüchtig geändert wird, wird die Variable sofort nach der Änderung in den Hauptspeicher geleert. Wenn andere Threads die Variable lesen müssen, lesen sie den neuen Wert aus dem Hauptspeicher. Gewöhnliche Variablen können dies nicht garantieren.

Neben dem volatilen Schlüsselwort können auch final und synchronisiert Sichtbarkeit erreichen.

Das Prinzip der Synchronisierung besteht darin, dass die gemeinsam genutzten Variablen nach der Ausführung mit dem Hauptspeicher synchronisiert werden müssen, bevor sie entsperrt werden.

Endgültig geänderte Felder sind nach der Initialisierung für andere Threads sichtbar, wenn kein Objekt entkommt (was bedeutet, dass das Objekt nach Abschluss der Initialisierung von anderen Threads verwendet werden kann).

Ordnung

In Java können Sie synchronisiert oder flüchtig verwenden, um die Ordnung von Vorgängen zwischen mehreren Threads sicherzustellen. Es gibt einige Unterschiede in den Implementierungsprinzipien: Das Schlüsselwort

volatile verwendet Speicherbarrieren, um die Neuordnung von Anweisungen zu verhindern und die Ordnung sicherzustellen.

Das Prinzip der Synchronisierung besteht darin, dass ein Thread nach dem Sperren entsperrt werden muss, bevor andere Threads erneut gesperrt werden können, sodass die von der Synchronisierung umschlossenen Codeblöcke seriell zwischen mehreren Threads ausgeführt werden.

Drei und acht Arten von Speicherinteraktionsoperationen

Es gibt 8 Arten von Speicherinteraktionsoperationen:

  • sperren (lock) wirkt auf Variablen im Hauptspeicher und markiert Variablen als Thread-exklusive Zustände.

  • lesen (lesen), wirkt auf Variablen im Hauptspeicher und überträgt den Wert der Variablen für den nächsten Ladevorgang vom Hauptspeicher in den Arbeitsspeicher des Threads verwenden.

  • laden (laden), wirkt auf die Variablen des Arbeitsspeichers und legt die Variablen des Lesevorgangs im Hauptspeicher in die Variablenkopie des Arbeitsspeichers ab.

  • verwenden (verwenden), wirken auf Variablen im Arbeitsspeicher, übertragen die Variablen im Arbeitsspeicher an die Ausführungsmaschine, wann immer die virtuelle Maschine auf eine Variable trifft, die benötigt wird zu verwenden. Diese Operation wird ausgeführt, wenn die Bytecode-Anweisung des Werts angegeben wird.

  • assign (Zuweisung), die auf eine Variable im Arbeitsspeicher einwirkt. Sie weist einen von der Ausführungs-Engine empfangenen Wert einer Kopie der Variablen im Arbeitsspeicher zu . Wann immer Die virtuelle Maschine führt diesen Vorgang aus, wenn sie auf eine Bytecode-Anweisung stößt, die einer Variablen einen Wert zuweist.

  • store (Speicher), eine Variable, die auf den Arbeitsspeicher einwirkt. Sie überträgt den Wert einer Variablen aus dem Arbeitsspeicher in den Hauptspeicher zur späteren Schreibverwendung. .

  • 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关键字

很多并发编程都使用了volatile关键字,主要的作用包括两点:

  • 保证线程间变量的可见性。

  • 禁止CPU进行指令重排序。

可见性

volatile修饰的变量,当一个线程改变了该变量的值,其他线程是立即可见的。普通变量则需要重新读取才能获得最新值。

volatile保证可见性的流程大概就是这个一个过程:

Analyse von JMM-Programmierbeispielen mit hoher Parallelität in Java

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的执行特性,最大限度的发挥机器的性能,提高程序的执行效率,只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。

重排序的种类分为三种,分别是:编译器重排序,指令级并行的重排序,内存系统重排序。整个过程如下所示:

Analyse von JMM-Programmierbeispielen mit hoher Parallelität in Java

指令重排序在单线程是没有问题的,不会影响执行结果,而且还提高了性能。但是在多线程的环境下就不能保证一定不会影响执行结果了。

所以在多线程环境下,就需要禁止指令重排序。

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是可见的。

volatile禁止指令重排序的原理

首先要讲一下内存屏障,内存屏障可以分为以下几类:

  • LoadLoad-Barriere: Für Anweisungen wie Load1, LoadLoad, Load2. Vor dem Zugriff auf die zu lesenden Daten in Load2 und nachfolgenden Lesevorgängen wird sichergestellt, dass die zu lesenden Daten in Load1 gelesen wurden.

  • StoreStore-Barriere: Stellen Sie bei solchen Anweisungen Store1, StoreStore, Store2 sicher, dass der Schreibvorgang von Store1 für andere Prozessoren sichtbar ist, bevor Store2 und nachfolgende Schreibvorgänge ausgeführt werden.

  • LoadStore-Barriere: Stellen Sie bei Anweisungen wie Load1, LoadStore, Store2 sicher, dass die von Load1 zu lesenden Daten gelesen werden, bevor Store2 und nachfolgende Schreibvorgänge gelöscht werden. vollständig.

  • StoreLoad-Barriere: Stellen Sie bei solchen Anweisungen Store1, StoreLoad, Load2 sicher, dass der Schreibvorgang in Store1 für alle Prozessoren sichtbar ist, bevor Load2 und alle nachfolgenden Lesevorgänge ausgeführt werden.

Fügen Sie nach jedem flüchtigen Lesevorgang eine LoadLoad-Barriere und nach einem Lesevorgang eine LoadStore-Barriere ein.

Analyse von JMM-Programmierbeispielen mit hoher Parallelität in Java

Fügen Sie vor jedem flüchtigen Schreibvorgang eine StoreStore-Barriere und hinten eine SotreLoad-Barriere ein.

Analyse von JMM-Programmierbeispielen mit hoher Parallelität in Java

Das obige ist der detaillierte Inhalt vonAnalyse von JMM-Programmierbeispielen mit hoher Parallelität in Java. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:yisu.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen