web-dev-qa-db-fra.com

Performance de la section synchronize en Java

J'ai eu une petite dispute sur les performances du bloc synchronized en Java. C'est une question théorique, qui n'affecte pas l'application réelle. 

Envisagez une application à thread unique, qui utilise des verrous et synchronise des sections. Ce code fonctionne-t-il plus lentement que le même code sans synchroniser les sections? Si oui, pourquoi? Nous ne discutons pas de la concurrence, car il s’agit uniquement d’une application à un seul thread

Mettre à jour

Trouvé intéressant référence le tester. Mais c'est à partir de 2001. Les choses auraient pu changer radicalement dans la dernière version de JDK

32
Anton

Il y a 3 types de verrouillage dans HotSpot

  1. Fat: la machine virtuelle Java s'appuie sur les mutex du système d'exploitation pour acquérir le verrou.
  2. Thin: la machine virtuelle Java utilise l'algorithme CAS. 
  3. Biased: CAS est une opération assez coûteuse sur certaines architectures. Verrouillage biaisé - type de verrouillage spécial optimisé pour le scénario lorsqu'un seul thread travaille sur un objet.

Par défaut, JVM utilise le verrouillage thin. Plus tard, si la machine virtuelle Java détermine qu'il n'y a pas de conflit de contention, le verrouillage fin est converti en verrouillage partialed. Les opérations qui modifient le type de verrou sont assez coûteuses et JVM n’applique donc pas cette optimisation immédiatement. Il existe une option spéciale de la machine virtuelle Java - XX: BusedLockingStartupDelay = delay, qui indique à la machine virtuelle Java quand ce type d'optimisation doit être appliqué.

Une fois polarisé, ce fil peut ensuite verrouiller et déverrouiller l'objet sans recourir à des instructions atomiques coûteuses. 

Réponse à la question: ça dépend. Mais s'il est biaisé, le code à thread unique avec verrouillage et sans verrouillage a des performances égales moyennes.

31
Anton

Le code à un seul thread fonctionnera toujours plus lentement si vous utilisez des blocs synchronized. Il est évident que vous ne bloquerez pas d'autres threads en attendant que d'autres threads soient terminés. Cependant, vous devrez gérer les autres effets de la synchronisation, à savoir la cohérence du cache.

Les blocs synchronisés ne sont pas seulement utilisés pour concurrency, mais aussi Visibility. Chaque bloc synchronisé est une barrière de mémoire: la JVM est libre de travailler sur des variables dans des registres, au lieu de la mémoire principale, en supposant que plusieurs threads n'accéderont pas à cette variable. Sans blocs de synchronisation, ces données pourraient être stockées dans le cache d'une CPU et différents threads sur différentes CPU ne verraient pas les mêmes données. En utilisant un bloc de synchronisation, vous forcez la machine virtuelle Java à écrire ces données dans la mémoire principale pour une visibilité sur les autres threads.

Ainsi, même si vous n’êtes plus en conflit de verrou, la machine virtuelle Java devra toujours effectuer des tâches de maintenance pour vider les données dans la mémoire principale.

De plus, cela a des contraintes d'optimisation. La JVM est libre de réorganiser les instructions afin de fournir une optimisation: considérons un exemple simple:

foo++;
bar++;

contre:

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

Dans le premier exemple, le compilateur est libre de charger foo et bar en même temps, puis de les incrémenter tous les deux, puis de les sauvegarder tous les deux. Dans le deuxième exemple, le compilateur must exécute les opérations load/add/save sur foo, puis exécute les opérations load/add/save sur bar. Ainsi, la synchronisation peut avoir un impact sur la capacité du JRE d'optimiser les instructions.

(Un excellent livre sur le modèle de mémoire Java est/ Java Concurrency In Practice . De Brian Goetz)

44
Edward Thomson

L'obtention d'un verrou non contesté entraîne des frais généraux, mais il est très petit sur les machines virtuelles modernes.

Une optimisation d’exécution clé qui s’applique à ce cas s’appelle "verrouillage biaisé" et est expliquée dans le Livre blanc sur les performances de Java SE 6 .

Si vous souhaitez disposer de chiffres de performance pertinents pour votre machine virtuelle Java et votre matériel, vous pouvez créer un micro-benchmark pour essayer de mesurer cette surcharge.

19
NPE

L'utilisation de verrous lorsque vous n'en avez pas besoin ralentira votre application. Il pourrait être trop petit pour être mesuré ou étonnamment élevé. 

IMHO Souvent, la meilleure approche consiste à utiliser du code sans verrouillage dans un programme à thread unique pour indiquer clairement que ce code n'est pas destiné à être partagé sur plusieurs threads. Cela pourrait être plus important pour la maintenance que pour tout problème de performances.

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

empreintes

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

Du point de vue des performances, si 4 ns est important pour vous, vous devez utiliser la version non synchronisée. 

Dans 99% des cas d'utilisation, la clarté du code est plus importante que les performances. Un code simple et clair donne souvent aussi de bons résultats.

BTW: J'utilise un i7 2600 à 4,6 GHz avec Oracle Java 7u1.


Pour comparaison si je fais ce qui suit où perfTest1,2,3 sont identiques.

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

Je reçois 

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

Si j'utilise une méthode perfTest commune, il ne peut pas intégrer le code de manière aussi optimale et ils sont tous plus lents

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

Échange de l'ordre des tests

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

un à la fois

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

et

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

En supposant que vous utilisiez la machine virtuelle HotSpot, je pense que la machine virtuelle est capable de reconnaître qu’il n’ya aucune contention pour les ressources du bloc synchronized et de la traiter comme du code "normal".

0
sworisbreathing

Cet exemple de code (avec 100 threads réalisant 1 000 000 itérations chacune) illustre la différence de performances entre éviter et ne pas éviter un bloc synchronisé.

Sortie:

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