2016-04-25 4 views
0

Ich habe eine Multi-Thread-Anwendung, in der Threads eine globale Variable verwenden. Und diese Variable wird nur einmal im Haupt-Thread aktualisiert.Wie erzwinge, dass alle Threads den letzten Wert einer Variablen verwenden, ohne flüchtig zu sein?

Was wäre der beste Weg, um sicherzustellen, dass alle anderen Threads den neuesten Wert haben, wenn sie aktualisiert werden?

Die Definition dieser Variable als volatil würde die Performance beeinträchtigen, da ich sie nur einmal in Tagen aktualisieren muss.

+1

Setzen Sie die Zuweisung in einen "synchronisierten" Block. –

+0

@AndyTurner Und ist das effizienter für Lesevorgänge? –

+0

Die Lesevorgänge sind nicht im synchronisierten Block. Nur die Zuordnung. Sie müssten den Monitor entsprechend auswählen. –

Antwort

0

Deklarieren Sie Ihre Variable als volatile und Sie sind fertig.


... Aber es ist in einigen Fällen möglich, den Zugriff auf Ihre Variable zu optimieren. Sie möchten 100% sicher sein, dass keine anderen Variablen die Cache-Zeile mit Ihrer Variablen teilen. Warum? Wenn keine Freigabe erfolgt, ist die Kopie der neuesten Version der Variablen in CPU-Caches aller Cores im S (gemeinsam genutzten) Zustand verfügbar. Das Lesen dieser Variablen ist schnell, als ob sie nicht als volatile deklariert wäre. Wenn jedoch andere Daten Cache-Zeile mit Ihrer Variablen teilen und diese anderen Daten häufig geändert werden (zB jede Mikrosekunde), dann werden Threads, die Ihre Variable lesen wollen, blockiert, während der letzte Wert Ihrer Variablen aus dem Cache der CPU übertragen wird, von dem dies geschieht Weitere Daten wurden aktualisiert.

Um oben genannten Szenarien, hier das Programm zu zeigen:

import java.util.concurrent.TimeUnit; 

public class CacheLineSharing { 

    static class Holder { 
     volatile long pad0; 
     volatile long pad1; 
     volatile long pad2; 
     volatile long pad3; 
     volatile long pad4; 
     volatile long pad5; 
     volatile long pad6; // either pad6 or pad7 shares cache line with x 
     volatile long x = INIT_VALUE; 
     volatile long pad7; 
     volatile long pad8; 
     volatile long pad9; 
     volatile long pad10; 
     volatile long pad11; 
     volatile long pad12; 
     volatile long pad13; 

     volatile long pad14; // definitely in different cache line than x 
    } 

    static final int INIT_VALUE = 0; 
    static final int START_VALUE = 1; 
    static final int FINISH_VALUE = 2; 

    static class MyThread extends Thread { 
     Holder holder; 
     MyThread(Holder holder) { 
      this.holder = holder; 
     } 
     @Override 
     public void run() { 
      while (holder.x != START_VALUE); 

      long cycles = 0; 
      while (holder.x != FINISH_VALUE) { 
       cycles++; 
      } 
      System.out.println(String.format("cycles=%d", cycles)); 
     } 
    } 

    public static void main(String[] args) throws Exception { 
     Holder holder = new Holder(); 

     for (int i = 0; i < 4; i++) { 
      new MyThread(holder).start(); 
     } 

     long ts = System.nanoTime(); 
     holder.x = START_VALUE; 
     for (int i = 0; i < 1000000000; i++) { 
      //holder.pad6 = i; 
      holder.pad14 = i; 
     } 
     holder.x = FINISH_VALUE; 
     long nanoes = System.nanoTime() - ts; 
     System.out.println(String.format("millis=%d", TimeUnit.NANOSECONDS.toMillis(nanoes))); 
    } 
} 

Unsere Zielgröße ist x und wir messen, wie schnell das Lesen sein Wert ist. Im ersten Fall wir pad6 Variable (es hat 6 in 7 Chancen des Teilen Cache-Zeile mit x) ständig aktualisieren:

millis=4317 
cycles=584637722 
cycles=655006968 
cycles=1214910177 
cycles=1133123641 

~ 280K Iteration pro Sekunde für schnellsten Thread

nun das gleiche Programm-Updates pad14 Variable, die Cache-Zeile mit x (modernem Intel-CPUs hat Cache-Zeilen von 64 Bytes = 8 Java-long-Positionen) nicht teilt:

cycles=1857323945 
cycles=2034309531 
cycles=1820202891 
millis=1363 
cycles=2083430201 

~ 1335K Iterationen pro Sekunde auch für slowe st Gewinde.

4,75-mal schneller!

Also, wenn Sie total verrückt nach Optimierung sind, pad Ihre Variable 7 Longs vor und 7 Longs nach.

+1

Das Vermeiden von falschem Teilen garantiert nicht sofort die Sichtbarkeit über Threads hinweg, selbst für einen einzelnen Schreiber. Das Auffüllen der Variablen stellt in keiner Weise sicher, dass Änderungen daran sofort an das Cachesubsystem weitergegeben werden, anstatt sie im Speicherpuffer zu belassen. Denken Sie auch daran, dass das Auffüllen mit einer fest codierten Anzahl von '' '' '' '' '' '' ''nicht portierbar ist (POWER8 verwendet 128-Byte-Cache-Zeilen) - wenn Sie Java 8 verwenden, verwenden Sie einfach' @ Contened'. Und wenn Sie nicht ein Werkzeug wie JMH verwenden, ist es schwer zu sagen, dass Sie keine von einer Reihe von unzusammenhängenden und subtilen Nebenwirkungen messen. –

+0

@DimitarDimitrov, wenn wir über moderne Intel-CPUs sprechen, dann sind Änderungen sofort sichtbar. Ausnahmen von dieser Regel sind: 1) Verwenden von SIMD-Anweisungen (die Ergebnisse im Speicherpuffer puffern können); 2) Verwenden von Speicheradressbereichen, die explizit mit einem Speichertyp mit schwächeren Garantien gekennzeichnet sind als ein Zurückschreiben im Cache. (Diese Lösung ist natürlich nicht auf andere Architekturen übertragbar. Das Problem besteht normalerweise darin, dass die Portabilität der Leistung widerspricht.) – gudok