Heim  >  Artikel  >  Java  >  Erfahren Sie, wie Sie flüchtige Variablen verstehen – gleichzeitige Java-Programmierung und Technologie-Insider

Erfahren Sie, wie Sie flüchtige Variablen verstehen – gleichzeitige Java-Programmierung und Technologie-Insider

php是最好的语言
php是最好的语言Original
2018-07-26 15:29:521405Durchsuche

Die Java-Sprache bietet einen etwas schwächeren Synchronisationsmechanismus, nämlich flüchtige Variablen, um sicherzustellen, dass andere Threads über Variablenaktualisierungsvorgänge benachrichtigt werden. Volatile Leistung: Der Leseleistungsverbrauch von Volatilität ist fast derselbe wie bei gewöhnlichen Variablen, der Schreibvorgang ist jedoch etwas langsamer, da viele Speicherbarrierenanweisungen in den nativen Code eingefügt werden müssen, um sicherzustellen, dass der Prozessor nicht in der falschen Reihenfolge ausgeführt wird.

1. Flüchtige Variablen

Die Java-Sprache bietet einen etwas schwächeren Synchronisierungsmechanismus, nämlich flüchtige Variablen , um sicherzustellen, dass andere Threads über Variablenaktualisierungsvorgänge benachrichtigt werden. Wenn eine Variable als flüchtig deklariert wird, bemerken der Compiler und die Laufzeit, dass die Variable gemeinsam genutzt wird, und daher werden Operationen an der Variablen nicht mit anderen Speicheroperationen neu angeordnet. Flüchtige Variablen werden nicht in Registern zwischengespeichert oder sind auf andere Weise für andere Prozessoren unsichtbar. Daher wird beim Lesen einer Variablen vom flüchtigen Typ immer der zuletzt geschriebene Wert zurückgegeben.

Beim Zugriff auf flüchtige Variablen wird kein Sperrvorgang durchgeführt, sodass der Ausführungsthread nicht blockiert wird. Daher sind flüchtige Variablen ein einfacherer Synchronisierungsmechanismus als das Schlüsselwort synchronisiert.

Erfahren Sie, wie Sie flüchtige Variablen verstehen – gleichzeitige Java-Programmierung und Technologie-Insider

Beim Lesen oder Schreiben nichtflüchtiger Variablen kopiert jeder Thread zunächst die Variable aus dem Speicher in den CPU-Cache. Wenn der Computer über mehrere CPUs verfügt, kann jeder Thread auf einer anderen CPU verarbeitet werden, was bedeutet, dass jeder Thread in einen anderen CPU-Cache kopiert werden kann.

Und die deklarierte Variable ist flüchtig. Die JVM stellt sicher, dass die Variable jedes Mal, wenn sie gelesen wird, aus dem Speicher gelesen wird, wobei der CPU-Cache-Schritt übersprungen wird.

Wenn eine Variable als flüchtig definiert ist, weist sie zwei Eigenschaften auf:

1. Stellen Sie sicher, dass diese Variable für alle Threads sichtbar ist. Die „Sichtbarkeit“ entspricht hier der am Anfang erwähnten Wie oben erwähnt, stellt Volatile sicher, dass der neue Wert sofort mit dem Hauptspeicher synchronisiert und unmittelbar vor jeder Verwendung aus dem Hauptspeicher aktualisiert wird, wenn ein Thread den Wert dieser Variablen ändert. Gewöhnliche Variablen können dies jedoch nicht tun. Der Wert gewöhnlicher Variablen muss zwischen Threads über den Hauptspeicher übertragen werden (Einzelheiten finden Sie unter: Java-Speichermodell).

2. Deaktivieren Sie die Optimierung der Neuordnung von Anweisungen. Bei Variablen mit flüchtigen Änderungen wird nach der Zuweisung eine zusätzliche Operation „load addl $0x0, (%esp)“ ausgeführt. Diese Operation entspricht einer Speicherbarriere (wenn Anweisungen neu angeordnet werden, können nachfolgende Anweisungen nicht an die Position vor dem Speicher neu angeordnet werden Barriere) ), wenn nur eine CPU auf den Speicher zugreift, ist keine Speicherbarriere erforderlich. (Was ist Befehlsneuordnung: Dies bedeutet, dass die CPU eine Methode verwendet, die es ermöglicht, mehrere Befehle separat an jede entsprechende Schaltkreiseinheit zur Verarbeitung in einer Reihenfolge zu senden nicht vom Programm angegeben).

flüchtige Leistung:

Der Leseleistungsverbrauch von flüchtigen Variablen ist fast der gleiche wie bei gewöhnlichen Variablen, der Schreibvorgang ist jedoch etwas langsamer, da viele Speicherbarriereanweisungen in den nativen Code eingefügt werden müssen um sicherzustellen, dass der Prozessor keine Ausführung außerhalb der Reihenfolge durchführt.

2. Speichersichtbarkeit

Aufgrund des Java Memory Model (JMM) werden alle Variablen im Hauptspeicher gespeichert und jeder Thread verfügt über einen eigenen Arbeitsspeicher (Cache).

Wenn der Thread funktioniert, muss er die Daten im Hauptspeicher in den Arbeitsspeicher kopieren. Auf diese Weise basiert jede Operation an den Daten auf dem Arbeitsspeicher (die Effizienz wird verbessert), und die Daten im Hauptspeicher und im Arbeitsspeicher anderer Threads können nicht direkt manipuliert werden, und dann werden die aktualisierten Daten im Hauptspeicher aktualisiert Erinnerung.

Der hier erwähnte Hauptspeicher kann einfach als Heap-Speicher betrachtet werden, während der Arbeitsspeicher als Stapelspeicher betrachtet werden kann.

Bei gleichzeitiger Ausführung kann es also vorkommen, dass die von Thread B gelesenen Daten die Daten sind, bevor Thread A aktualisiert wird.

Natürlich wird dies definitiv Probleme verursachen, daher erscheint die Rolle von Volatile:

Wenn eine Variable durch Volatilität geändert wird, wird der Schreibvorgang eines Threads darauf sofort gelöscht den Hauptspeicher und erzwingt das Löschen der Daten im Thread, der die Variable zwischengespeichert hat, und die neuesten Daten müssen erneut aus dem Hauptspeicher gelesen werden.

Nach der flüchtigen Änderung ruft der Thread die Daten nicht direkt aus dem Hauptspeicher ab. Er muss die Variable weiterhin in den Arbeitsspeicher kopieren.

Anwendung der Speichersichtbarkeit

Wenn wir zwischen zwei Threads basierend auf dem Hauptspeicher kommunizieren müssen, muss die Kommunikationsvariable mit volatile geändert werden:

public class Test {

private static /*volatile*/ boolean stop = false;

public static void main(String[] args) throws Exception {
    Thread t = new Thread(
            () -> {
                int i = 0;
                while (!stop) {
                    i++;
                 System.out.println("hello");
                }
            });
    t.start();

    Thread.sleep(1000);
    TimeUnit.SECONDS.sleep(1);
    System.out.println("Stop Thread");
    stop = true;
}
}

Wenn das obige Beispiel zutrifft nicht auf „volatil“ eingestellt, wird der Thread möglicherweise nie beendet

Aber hier liegt ein Missverständnis vor. Diese Art der Verwendung kann leicht den Eindruck erwecken:

Gleichzeitige Operationen auf volatile-modified Variablen sind Thread-sicher.

Hier ist es wichtig zu betonen, dass volatile keine Thread-Sicherheit garantiert!

Das folgende Programm:

public class VolatileInc implements Runnable {

private static volatile int count = 0; //使用 volatile 修饰基本数据内存不能保证原子性

//private static AtomicInteger count = new AtomicInteger() ;

@Override

public void run() {
    for (int i = 0; i < 100; i++) {
        count++;
        //count.incrementAndGet() ;

    }
}

public static void main(String[] args) throws InterruptedException {
    VolatileInc volatileInc = new VolatileInc();
    IntStream.range(0,100).forEach(i->{
        Thread t= new Thread(volatileInc, "t" + i);
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    System.out.println(count);
}
}

Wenn unsere drei Threads (t1, t2, main) gleichzeitig einen int akkumulieren, werden wir feststellen, dass der Endwert kleiner ist als 100000.

这是因为虽然 volatile 保证了内存可见性,每个线程拿到的值都是最新值,但 count ++ 这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。

所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。也可以使用 synchronize 或者是锁的方式来保证原子性。还可以用 Atomic 包中 AtomicInteger 来替换 int,它利用了 CAS 算法来保证了原子性。

三、指令重排序

内存可见性只是 volatile 的其中一个语义,它还可以防止 JVM 进行指令重排优化。

举一个伪代码:

int a=10 ;//1
int b=20 ;//2
int c= a+b ;//3

一段特别简单的代码,理想情况下它的执行顺序是:1>2>3。但有可能经过 JVM 优化之后的执行顺序变为了 2>1>3。

可以发现不管 JVM 怎么优化,前提都是保证单线程中最终结果不变的情况下进行的。

可能这里还看不出有什么问题,那看下一段伪代码:

 private static Map<String,String> value ;
 private static volatile boolean flag = fasle ;
  //以下方法发生在线程 A 中 初始化 Map
 public void initMap(){
 //耗时操作
 value = getMapValue() ;//1
 flag = true ;//2
}

 //发生在线程 B中 等到 Map 初始化成功进行其他操作
public void doSomeThing(){
while(!flag){
    sleep() ;
}
 //dosomething
 doSomeThing(value);
}

这里就能看出问题了,当 flag 没有被 volatile 修饰时,JVM 对 1 和 2 进行重排,导致 value都还没有被初始化就有可能被线程 B 使用了。

所以加上 volatile 之后可以防止这样的重排优化,保证业务的正确性。

指令重排的的应用

一个经典的使用场景就是双重懒加载的单例模式了:

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;
}

这里的 volatile 关键字主要是为了防止指令重排。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

    1.给 instance 分配内存

    2.调用 Singleton 的构造函数来初始化成员变量

    3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。

       但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

相关文章:

具体介绍java高并发中volatile的实现原理

Java中如何正确使用Volatile变量?

相关视频:

Java多线程与并发库高级应用视频教程

Das obige ist der detaillierte Inhalt vonErfahren Sie, wie Sie flüchtige Variablen verstehen – gleichzeitige Java-Programmierung und Technologie-Insider. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn