it-swarm.com.de

Leistung des Synchronisierungsabschnitts in Java

Ich hatte einen kleinen Streit über die Leistung des synchronized-Blocks in Java. Dies ist eine theoretische Frage, die sich nicht auf die reale Anwendung auswirkt. 

Ziehen Sie eine Single-Thread-Anwendung in Betracht, die Sperren verwendet und Abschnitte synchronisiert. Funktioniert dieser Code langsamer als derselbe Code, ohne Abschnitte synchronisieren zu müssen? Wenn ja warum? Wir sprechen nicht über Parallelität, da es sich nur um eine Threadanwendung handelt

Update

Interessantes gefunden Benchmark zum Testen. Aber es ist aus dem Jahr 2001. In der neuesten Version von JDK könnte sich das dramatisch geändert haben

32
Anton

Es gibt drei Arten von Sperren in HotSpot

  1. Fat: JVM verwendet OS-Mutexe, um eine Sperre zu erhalten.
  2. Thin: JVM verwendet den CAS-Algorithmus. 
  3. Voreingenommenheit: CAS ist für einige Architekturen ziemlich teuer. Vorgespannte Verriegelung - ist eine spezielle Art der Verriegelung, die für Szenarien optimiert ist, in denen nur ein Thread an einem Objekt arbeitet.

Standardmäßig verwendet JVM die Sperre von thin. Wenn JVM später feststellt, dass kein Konflikt vorliegt, wird das Thin-Locking in das Bias-Locking umgewandelt. Ein Vorgang, bei dem die Art der Sperre geändert wird, ist ziemlich teuer, weshalb JVM diese Optimierung nicht sofort anwendet. Es gibt eine spezielle JVM-Option - XX: BiasedLockingStartupDelay = delay, die der JVM mitteilt, wann diese Art der Optimierung angewendet werden soll.

Nach dem Verzerren kann dieser Thread das Objekt anschließend sperren und entsperren, ohne auf teure atomare Anweisungen zurückgreifen zu müssen. 

Antwort auf die Frage: es kommt darauf an. Bei einer Verzerrung hat der Single-Threaded-Code mit Verriegelung und ohne Verriegelung im Durchschnitt die gleiche Leistung.

31
Anton

Single-Threaded-Code wird langsamer ausgeführt, wenn synchronized-Blöcke verwendet werden. Offensichtlich werden keine anderen Threads blockiert, während auf die Fertigstellung anderer Threads gewartet wird. Sie müssen sich jedoch mit den anderen Auswirkungen der Synchronisierung, nämlich der Cache-Kohärenz, befassen.

Synchronisierte Blöcke werden nicht nur für Parallelität verwendet, sondern auch für Sichtbarkeit. Jeder synchronisierte Block ist eine Speicherbarriere: Die JVM kann anstelle des Hauptspeichers mit Variablen in Registern arbeiten, sofern davon ausgegangen wird, dass nicht mehrere Threads auf diese Variable zugreifen. Ohne Synchronisationsblöcke könnten diese Daten im Cache einer CPU gespeichert werden, und unterschiedliche Threads auf verschiedenen CPUs würden nicht dieselben Daten sehen. Durch Verwendung eines Synchronisationsblocks zwingen Sie die JVM, diese Daten in den Hauptspeicher zu schreiben, um sie für andere Threads sichtbar zu machen.

Auch wenn Sie keine Sperrkonflikte haben, muss die JVM trotzdem die Housekeeping-Funktion ausführen, um Daten in den Hauptspeicher zu leeren.

Darüber hinaus hat dies Optimierungseinschränkungen. Die JVM kann Anweisungen nachbestellen, um eine Optimierung zu ermöglichen: Ein einfaches Beispiel:

foo++;
bar++;

gegen:

foo++;
synchronized(obj)
{
    bar++;
}

Im ersten Beispiel kann der Compiler foo und bar gleichzeitig laden, dann beide inkrementieren und dann beide speichern. Im zweiten Beispiel führt der Compiler must das Laden/Hinzufügen/Speichern von foo und dann das Laden/Hinzufügen/Speichern von bar aus. Daher kann die Synchronisierung die Fähigkeit der JRE zur Optimierung von Anweisungen beeinträchtigen.

(Ein hervorragendes Buch über das Java-Speichermodell ist Brian Goetz Java Concurrency In Practice .)

44
Edward Thomson

Der Erwerb einer nicht angefochtenen Sperre ist mit einem gewissen Aufwand verbunden, bei modernen JVMs ist sie jedoch sehr gering.

Eine wichtige Laufzeitoptimierung, die für diesen Fall relevant ist, wird als "Vorgespanntes Sperren" bezeichnet und im Java SE 6 Performance-Whitepaper erläutert.

Wenn Sie einige für Ihre JVM und Hardware relevante Leistungszahlen wünschen, können Sie einen Mikro-Benchmark erstellen, um diesen Overhead zu messen.

19
NPE

Wenn Sie Sperren verwenden, wenn Sie dies nicht benötigen, wird Ihre Anwendung langsamer. Es kann zu klein sein, um gemessen zu werden, oder es kann überraschend hoch sein. 

IMHO Häufig ist es die beste Methode, sperrungsfreien Code in einem einzelnen Thread-Programm zu verwenden, um deutlich zu machen, dass dieser Code nicht für den gemeinsamen Thread gedacht ist. Dies kann für die Wartung wichtiger sein als alle Leistungsprobleme.

public static void main(String... args) throws IOException {
    for (int i = 0; i < 3; i++) {
        perfTest(new Vector<Integer>());
        perfTest(new ArrayList<Integer>());
    }
}

private static void perfTest(List<Integer> objects) {
    long start = System.nanoTime();
    final int runs = 100000000;
    for (int i = 0; i < runs; i += 20) {
        // add items.
        for (int j = 0; j < 20; j+=2)
            objects.add(i);
        // remove from the end.
        while (!objects.isEmpty())
            objects.remove(objects.size() - 1);
    }
    long time = System.nanoTime() - start;
    System.out.printf("%s each add/remove took an average of %.1f ns%n", objects.getClass().getSimpleName(),  (double) time/runs);
}

druckt

Vector each add/remove took an average of 38.9 ns
ArrayList each add/remove took an average of 6.4 ns
Vector each add/remove took an average of 10.5 ns
ArrayList each add/remove took an average of 6.2 ns
Vector each add/remove took an average of 10.4 ns
ArrayList each add/remove took an average of 5.7 ns

Wenn aus Sicht der Leistung 4 ns für Sie wichtig sind, müssen Sie die nicht synchronisierte Version verwenden. 

In 99% der Anwendungsfälle ist die Klarheit des Codes wichtiger als die Leistung. Klarer, einfacher Code funktioniert oft auch recht gut.

Übrigens: Ich verwende einen 4,6 GHz i7 2600 mit Oracle Java 7u1.


Zum Vergleich, wenn ich folgendes mache, wobei perfTest1,2,3 identisch ist.

    perfTest1(new ArrayList<Integer>());
    perfTest2(new Vector<Integer>());
    perfTest3(Collections.synchronizedList(new ArrayList<Integer>()));

Ich bekomme 

ArrayList each add/remove took an average of 2.6 ns
Vector each add/remove took an average of 7.5 ns
SynchronizedRandomAccessList each add/remove took an average of 8.9 ns

Wenn ich eine gängige perfTest-Methode verwende, kann sie den Code nicht optimal integrieren und sie sind alle langsamer

ArrayList each add/remove took an average of 9.3 ns
Vector each add/remove took an average of 12.4 ns
SynchronizedRandomAccessList each add/remove took an average of 13.9 ns

Die Reihenfolge der Tests vertauschen

ArrayList each add/remove took an average of 3.0 ns
Vector each add/remove took an average of 39.7 ns
ArrayList each add/remove took an average of 2.0 ns
Vector each add/remove took an average of 4.6 ns
ArrayList each add/remove took an average of 2.3 ns
Vector each add/remove took an average of 4.5 ns
ArrayList each add/remove took an average of 2.3 ns
Vector each add/remove took an average of 4.4 ns
ArrayList each add/remove took an average of 2.4 ns
Vector each add/remove took an average of 4.6 ns

eins nach dem anderen

ArrayList each add/remove took an average of 3.0 ns
ArrayList each add/remove took an average of 3.0 ns
ArrayList each add/remove took an average of 2.3 ns
ArrayList each add/remove took an average of 2.2 ns
ArrayList each add/remove took an average of 2.4 ns

und

Vector each add/remove took an average of 28.4 ns
Vector each add/remove took an average of 37.4 ns
Vector each add/remove took an average of 7.6 ns
Vector each add/remove took an average of 7.6 ns
Vector each add/remove took an average of 7.6 ns
9
Peter Lawrey

Wenn Sie davon ausgehen, dass Sie die HotSpot-VM verwenden, kann die JVM meiner Meinung nach erkennen, dass es keine Konflikte mit Ressourcen im synchronized-Block gibt, und behandeln Sie sie als "normalen" Code.

0
sworisbreathing

Dieser Beispielcode (mit 100 Threads mit jeweils 1.000.000 Iterationen) veranschaulicht den Leistungsunterschied zwischen dem Vermeiden und Nicht-Vermeiden eines synchronisierten Blocks.

Ausgabe:

Total time(Avoid Sync Block): 630ms
Total time(NOT Avoid Sync Block): 6360ms
Total time(Avoid Sync Block): 427ms
Total time(NOT Avoid Sync Block): 6636ms
Total time(Avoid Sync Block): 481ms
Total time(NOT Avoid Sync Block): 5882ms

Code:

import org.Apache.commons.lang.time.StopWatch;

public class App {
    public static int countTheads = 100;
    public static int loopsPerThead = 1000000;
    public static int sleepOfFirst = 10;

    public static int runningCount = 0;
    public static Boolean flagSync = null;

    public static void main( String[] args )
    {        
        for (int j = 0; j < 3; j++) {     
            App.startAll(new App.AvoidSyncBlockRunner(), "(Avoid Sync Block)");
            App.startAll(new App.NotAvoidSyncBlockRunner(), "(NOT Avoid Sync Block)");
        }
    }

    public static void startAll(Runnable runnable, String description) {
        App.runningCount = 0;
        App.flagSync = null;
        Thread[] threads = new Thread[App.countTheads];

        StopWatch sw = new StopWatch();
        sw.start();
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(runnable);
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
        do {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } while (runningCount != 0);
        System.out.println("Total time"+description+": " + (sw.getTime() - App.sleepOfFirst) + "ms");
    }

    public static void commonBlock() {
        String a = "foo";
        a += "Baa";
    }

    public static synchronized void incrementCountRunning(int inc) {
        runningCount = runningCount + inc;
    }

    public static class NotAvoidSyncBlockRunner implements Runnable {

        public void run() {
            App.incrementCountRunning(1);
            for (int i = 0; i < App.loopsPerThead; i++) {
                synchronized (App.class) {
                    if (App.flagSync == null) {
                        try {
                            Thread.sleep(App.sleepOfFirst);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        App.flagSync = true;
                    }
                }
                App.commonBlock();
            }
            App.incrementCountRunning(-1);
        }
    }

    public static class AvoidSyncBlockRunner implements Runnable {

        public void run() {
            App.incrementCountRunning(1);
            for (int i = 0; i < App.loopsPerThead; i++) {
                // THIS "IF" MAY SEEM POINTLESS, BUT IT AVOIDS THE NEXT 
                //ITERATION OF ENTERING INTO THE SYNCHRONIZED BLOCK
                if (App.flagSync == null) {
                    synchronized (App.class) {
                        if (App.flagSync == null) {
                            try {
                                Thread.sleep(App.sleepOfFirst);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            App.flagSync = true;
                        }
                    }
                }
                App.commonBlock();
            }
            App.incrementCountRunning(-1);
        }
    }
}
0
Hailton