Heim  >  Artikel  >  Java  >  Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

WBOY
WBOYnach vorne
2023-05-28 11:23:391273Durchsuche

Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

F: Bitte teilen Sie uns Ihr Verständnis von Volatilität mit?
Antwort: Volatile ist ein leichter Synchronisierungsmechanismus, der von der Java Virtual Machine bereitgestellt wird. Er verfügt über drei Funktionen:
1) Garantierte Sichtbarkeit
2) Garantiert keine Atomizität
3) Verhindert die Neuordnung von Anweisungen

Ich bin gerade fertig Erlernen der Grundlagen von Java. Wenn Sie jemand fragt, was flüchtig ist? Wenn es eine Funktion hat, müssen Sie meiner Meinung nach sehr verwirrt sein ...
Vielleicht verstehen Sie nach dem Lesen der Antwort überhaupt nicht, was der Synchronisationsmechanismus ist? Was ist Sichtbarkeit? Was ist Atomizität? Was ist eine Neuordnung von Anweisungen?

1. Volatile garantiert Sichtbarkeit

1.1 Was ist das JMM-Modell?

Um zu verstehen, was Sichtbarkeit ist, müssen Sie zunächst JMM verstehen.

JMM (Java Memory Model) selbst ist ein abstraktes Konzept und existiert nicht wirklich. Es beschreibt eine Reihe von Regeln oder Spezifikationen. Durch diese Reihe von Spezifikationen werden die Zugriffsmethoden verschiedener Variablen im Programm festgelegt. JMMs Regelungen zur Synchronisation:
1) Bevor ein Thread entsperrt wird, muss der Wert der gemeinsam genutzten Variablen wieder im Hauptspeicher aktualisiert werden
2) Bevor der Thread gesperrt wird, muss der neueste Wert des Hauptspeichers in ihn eingelesen werden eigener Arbeitsspeicher;
3) Das Entsperren einer Sperre ist die gleiche Sperre

Da die Entität des laufenden JVM-Programms ein Thread ist, erstellt das JMM beim Erstellen jedes Threads einen Arbeitsspeicher (an manchen Stellen als Stapelspeicher bezeichnet). Der Arbeitsspeicher besteht aus den privaten Daten jedes Threads.

Das Java-Speichermodell schreibt vor, dass alle Variablen im Hauptspeicher gespeichert werden. Der Hauptspeicher ist ein gemeinsamer Speicherbereich, auf den alle Threads zugreifen können.

Aber die Operationen des Threads an Variablen (Lesen, Zuweisen usw.) müssen im Arbeitsspeicher ausgeführt werden. Zuerst müssen Sie die Variablen vom Hauptspeicher in den Arbeitsspeicher kopieren, Operationen ausführen und sie dann wieder in den Hauptspeicher schreiben.

Nachdem Sie die obige Einführung in JMM gelesen haben, sind Sie möglicherweise immer noch verwirrt über die Vorteile. Nehmen wir als Beispiel ein Ticketverkaufssystem:

1) Wie unten gezeigt, ist derzeit nur noch 1 Ticket im Backend übrig des Ticketverkaufssystems und wurde in den Hauptspeicher eingelesen: TicketNum=1.
2) Zu diesem Zeitpunkt gibt es mehrere Benutzer im Netzwerk, die sich Tickets schnappen, daher gibt es mehrere Threads, die gleichzeitig Ticketkaufdienste durchführen. Gehen Sie davon aus, dass es 3 Threads gibt, die die aktuelle Anzahl an Tickets gelesen haben Diesmal: ticketNum=1 , dann kaufe ich ein Ticket.
3) Angenommen, Thread 1 beansprucht zuerst die CPU-Ressourcen, kauft zuerst das Ticket und ändert den Wert von TicketNum in seinem eigenen Arbeitsspeicher auf 0: TicketNum = 0, und schreibt es dann zurück in den Hauptspeicher.

Zu diesem Zeitpunkt hat der Benutzer in Thread 1 das Ticket bereits gekauft, sodass Thread 2 und Thread 3 zu diesem Zeitpunkt nicht in der Lage sein sollten, weiterhin Tickets zu kaufen. Daher muss das System Thread 2 und Thread 3 darüber informieren, dass die Ticketnummer lautet jetzt gleich 0: TicketNum =0. Wenn es einen solchen Benachrichtigungsvorgang gibt, können Sie ihn als Sichtbarkeit verstehen.

Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

Anhand der obigen Einführung und Beispiele von JMM können wir es kurz zusammenfassen.

Die Sichtbarkeit des JMM-Speichermodells bedeutet, dass, wenn mehrere Threads auf eine Ressource im Hauptspeicher zugreifen und ein Thread die Ressource in seinem eigenen Arbeitsspeicher ändert und sie zurück in den Hauptspeicher schreibt, das JMM-Speichermodell Andere benachrichtigen sollte Threads beziehen die neuesten Ressourcen erneut, um die Sichtbarkeit der neuesten Ressourcen sicherzustellen.

1.2. Codeüberprüfung der flüchtigen garantierten Sichtbarkeit

In Abschnitt 1.1 haben wir die Definition von Sichtbarkeit grundsätzlich verstanden und können nun Code verwenden, um die Definition zu überprüfen. Die Praxis hat gezeigt, dass der Einsatz von Volatilität tatsächlich für Sichtbarkeit sorgen kann.

1.2.1. Codeüberprüfung ohne Sichtbarkeit

Überprüfen Sie zunächst, ob keine Sichtbarkeit besteht, wenn flüchtig nicht verwendet wird.

package com.koping.test;import java.util.concurrent.TimeUnit;class MyData{
    int number = 0;

    public void add10() {
        this.number += 10;
    }}public class VolatileVisibilityDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动一个线程修改myData的number,将number的值加10
        new Thread(
                () -> {
                    System.out.println("线程" + Thread.currentThread().getName()+"\t 正在执行");
                    try{
                        TimeUnit.SECONDS.sleep(3);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    myData.add10();
                    System.out.println("线程" + Thread.currentThread().getName()+"\t 更新后,number的值为" + myData.number);
                }
        ).start();

        // 看一下主线程能否保持可见性
        while (myData.number == 0) {
            // 当上面的线程将number加10后,如果有可见性的话,那么就会跳出循环;
            // 如果没有可见性的话,就会一直在循环里执行
        }

        System.out.println("具有可见性!");
    }}

Das laufende Ergebnis ist wie unten dargestellt. Sie können sehen, dass sich der Hauptthread immer noch in der Schleife befindet, obwohl Thread 0 den Wert von Zahl auf 10 geändert hat, da Zahl zu diesem Zeitpunkt nicht sichtbar ist und das System keine aktive Benachrichtigung sendet .
Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

1.2.1, Volatile-Garantie-Sichtbarkeitsüberprüfung

Testen Sie erneut, nachdem Sie Volatilität zur Variablennummer in Zeile 7 des obigen Codes hinzugefügt haben, wie unten gezeigt. Zu diesem Zeitpunkt hat der Hauptthread die Schleife erfolgreich verlassen, da JMM aktiv benachrichtigt wurde Der Hauptthread Der Wert von Zahl wurde aktualisiert.
Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

2. Volatile garantiert keine Atomizität

2.1 Was ist Atomizität?

Nachdem wir die oben erwähnte Sichtbarkeit verstanden haben, wollen wir verstehen, was Atomizität ist.

Atomic bezieht sich auf die Eigenschaft, nicht geteilt oder unterbrochen werden zu können und die Integrität zu bewahren. Mit anderen Worten: Wenn ein Thread eine Operation ausführt, kann diese durch keinen Faktor unterbrochen werden. Entweder gelingt es gleichzeitig oder es scheitert gleichzeitig.

Es ist noch etwas abstrakt, geben wir ein Beispiel.

Wie unten gezeigt, wird eine Klasse zum Testen der Atomizität erstellt: TestPragma. Der kompilierte Code zeigt, dass die Erhöhung von n in der Add-Methode durch drei Anweisungen abgeschlossen wird.

因此可能存在线程1正在执行第1个指令,紧接着线程2也正在执行第1个指令,这样当线程1和线程2都执行完3个指令之后,很容易理解,此时n的值只加了1,而实际是有2个线程加了2次,因此这种情况就是不保证原子性。
Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

2.2 不保证原子性的代码验证

在2.1中已经进行了举例,可能存在2个线程执行n++的操作,但是最终n的值却只加了1的情况,接下来对这种情况再用代码进行演示下。

首先给MyData类添加一个add方法

package com.koping.test;class MyData {
    volatile int number = 0;

    public void add() {
        number++;
    }}

然后创建测试原子性的类:TestPragmaDemo。验证number的值是否为20000,需要测试通过20个线程分别对其加1000次后的结果。

package com.koping.test;public class TestPragmaDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
        for (int i=0; i<20; i++) {
            new Thread(() -> {
                for (int j=0; j<1000; j++) {
                    myData.add();
                }
            }).start();
        }

        // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);

    }}

运行结果如下图,最终number的值仅为18410。
可以看到即使加了volatile,依然不保证有原子性。
Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

2.3 volatile不保证原子性的解决方法

上面介绍并证明了volatile不保证原子性,那如果希望保证原子性,怎么办呢?以下提供了2种方法

2.3.1 方法1:使用synchronized

方法1是在add方法上添加synchronized,这样每次只有1个线程能执行add方法。

结果如下图,最终确实可以使number的值为20000,保证了原子性。

但在实际业务逻辑方法中,很少只有一个类似于number++的单行代码,通常会包含其他n行代码逻辑。现在为了保证number的值是20000,就把整个方法都加锁了(其实另外那n行代码,完全可以由多线程同时执行的)。所以就优点杀鸡用牛刀,高射炮打蚊子,小题大做了。

package com.koping.test;class MyData {
    volatile int number = 0;

    public synchronized void add() {
      // 在n++上面可能还有n行代码进行逻辑处理
        number++;
    }}

Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

2.3.2 方法1:使用JUC包下的AtomicInteger

给MyData新曾一个原子整型类型的变量num,初始值为0。

package com.koping.test;import java.util.concurrent.atomic.AtomicInteger;class MyData {
    volatile int number = 0;

    volatile AtomicInteger num = new AtomicInteger();

    public void add() {
        // 在n++上面可能还有n行代码进行逻辑处理
        number++;
        num.getAndIncrement();
    }}

让num也同步加20000次。可以将原句重写为:使用原子整型num可以确保原子性,如下图所示:在执行number++时不会发生竞态条件。

package com.koping.test;public class TestPragmaDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
        for (int i=0; i<20; i++) {
            new Thread(() -> {
                for (int j=0; j<1000; j++) {
                    myData.add();
                }
            }).start();
        }

        // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);
        System.out.println("num值加了20000次,此时number的实际值是:" + myData.num);

    }}

Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

3、volatile禁止指令重排

3.1 什么是指令重排?

在第2节中理解了什么是原子性,现在要理解下什么是指令重排?

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排:
源代码–>编译器优化重排–>指令并行重排–>内存系统重排–>最终执行指令

处理器在进行重排时,必须要考虑指令之间的数据依赖性。

单线程环境中,可以确保最终执行结果和代码顺序执行的结果一致。

但是多线程环境中,线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测

看了上面的文字性表达,然后看一个很简单的例子。
比如下面的mySort方法,在系统指令重排后,可能存在以下3种语句的执行情况:
1)1234
2)2134
3)1324
以上这3种重排结果,对最后程序的结果都不会有影响,也考虑了指令之间的数据依赖性。

public void mySort() {
    int x = 1;  // 语句1
    int y = 2;  // 语句2
    x = x + 3;  // 语句3
    y = x * x;  // 语句4}

3.2 单线程单例模式

看完指令重排的简单介绍后,然后来看下单例模式的代码。

package com.koping.test;public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        // 单线程测试
        System.out.println("单线程的情况测试开始");
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println("单线程的情况测试结束\n");
    }}

首先是在单线程情况下进行测试,结果如下图。可以看到,构造方法只执行了一次,是没有问题的。
Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

3.3 多线程单例模式

接下来在多线程情况下进行测试,代码如下。

package com.koping.test;public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }

        // DCL(Double Check Lock双端检索机制)//        if (instance == null) {//            synchronized (SingletonDemo.class) {//                if (instance == null) {//                    instance = new SingletonDemo();//                }//            }//        }
        return instance;
    }

    public static void main(String[] args) {
        // 单线程测试//        System.out.println("单线程的情况测试开始");//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println("单线程的情况测试结束\n");

        // 多线程测试
        System.out.println("多线程的情况测试开始");
        for (int i=1; i<=10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }}

在多线程情况下的运行结果如下图。可以看到,多线程情况下,出现了构造方法执行了2次的情况。
Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

3.4 多线程单例模式改进:DCL

在3.3中的多线程单里模式下,构造方法执行了两次,因此需要进行改进,这里使用双端检锁机制:Double Check Lock, DCL。即加锁之前和之后都进行检查。

package com.koping.test;public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {//        if (instance == null) {//            instance = new SingletonDemo();//        }

        // DCL(Double Check Lock双端检锁机制)
        if (instance == null) {  // a行
            synchronized (SingletonDemo.class) {
                if (instance == null) {  // b行
                    instance = new SingletonDemo();  // c行
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        // 单线程测试//        System.out.println("单线程的情况测试开始");//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println("单线程的情况测试结束\n");

        // 多线程测试
        System.out.println("多线程的情况测试开始");
        for (int i=1; i<=10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }}

在多次运行后,可以看到,在多线程情况下,此时构造方法也只执行1次了。
Analyse flüchtiger Anwendungsbeispiele der Java-Grundlagen

3.5 Verbesserungen des Multithread-Singleton-Modus, Probleme mit der DCL-Version

Es ist zu beachten, dass der Singleton-Modus der DCL-Version in 3.4 immer noch nicht 100 % genau ist! ! !

Verstehen Sie nicht ganz, warum die 3.4DCL-Version des Singleton-Modus nicht 100 % genau ist?
Verstehen Sie nicht ganz, warum wir plötzlich über den Multithread-Singleton-Modus sprechen müssen, nachdem wir das einfache Verständnis der Befehlsumordnung in 3.1 abgeschlossen haben?

Da die 3.4DCL-Version des Singleton-Modus aufgrund der Befehlsumordnung Probleme verursachen kann, ist der Code immer noch nicht 100 % genau, obwohl die Wahrscheinlichkeit dieses Problems bei eins zu zehn Millionen liegt. Wenn Sie eine 100-prozentige Genauigkeit gewährleisten möchten, müssen Sie das Schlüsselwort volatile hinzufügen. Das Hinzufügen von volatile kann die Neuanordnung von Befehlen verhindern.

Als nächstes analysieren wir, warum die 3.4DCL-Version des Singleton-Modus nicht 100 % genau ist.

View-Instanz = new SingletonDemo(); Die kompilierten Anweisungen können in die folgenden drei Schritte unterteilt werden:

1) Objektspeicherplatz zuweisen: Speicher = allocate();
3 ) Stellen Sie die Instanz so ein, dass sie auf die zugewiesene Speicheradresse zeigt: Instanz = Speicher;

Da zwischen den Schritten 2 und 3 keine Datenabhängigkeit besteht, kann Schritt 132 ausgeführt werden.

Zum Beispiel hat Thread 1 Schritt 13 ausgeführt, aber Schritt 2 nicht ausgeführt. Zu diesem Zeitpunkt ist Instanz!=Null, aber das Objekt wurde noch nicht initialisiert.

Wenn Thread 2 zu diesem Zeitpunkt die CPU belegt, wird diese Instanz gefunden !=null und kehrt dann zur direkten Verwendung zurück. Sie werden feststellen, dass die Instanz leer ist und eine Ausnahme auftritt.

Dies ist ein Problem, das durch eine Neuanordnung von Anweisungen verursacht werden kann. Wenn Sie also sicherstellen möchten, dass das Programm zu 100 % korrekt ist, müssen Sie volatile hinzufügen, um eine Neuanordnung von Anweisungen zu verhindern.

3.6 Das Prinzip der flüchtigen Garantien zur Verhinderung der Befehlsumordnung

In 3.1 haben wir kurz die Bedeutung der Ausführungsumordnung vorgestellt und dann in den Abschnitten 3.2 bis 3.5 den Singleton-Modus verwendet, um die Gründe zu veranschaulichen, warum flüchtige Befehle in mehreren Fällen verwendet werden sollten. Thread-Situationen, da es zu einer Neuanordnung der Befehle kommen kann, die Programmausnahmen verursachen.

Als nächstes werden wir das Volatilitätsprinzip einführen, um sicherzustellen, dass die Neuordnung von Anweisungen verboten ist.

Zuerst müssen wir ein Konzept verstehen: Gedächtnisbarriere, auch bekannt als Gedächtnisbarriere. Es handelt sich um eine CPU-Anweisung, die zwei Funktionen hat:

1) Gewährleistung der Ausführungsreihenfolge bestimmter Operationen; 2) Gewährleistung der Speichersichtbarkeit bestimmter Variablen


Da sowohl der Compiler als auch der Prozessor eine Befehlsumordnung durchführen können; Wenn eine Speicherbarriere zwischen Anweisungen eingefügt wird, teilt dies dem Compiler und der CPU mit, dass mit dieser Speicherbarriere-Anweisung keine Anweisungen neu angeordnet werden können. Mit anderen Worten: Durch das Einfügen einer Speicherbarriere wird verhindert, dass Anweisungen vor und nach der Speicherbarriere neu angeordnet werden -ausgeführt. Optimierung der Planungsanforderungen
.

Eine weitere Funktion der Speicherbarriere besteht darin, das Leeren der Cache-Daten verschiedener CPUs zu erzwingen, sodass jeder Thread auf der CPU die neueste Version dieser Daten lesen kann.

Das obige ist der detaillierte Inhalt vonAnalyse flüchtiger Anwendungsbeispiele der Java-Grundlagen. 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