web-dev-qa-db-fra.com

Implémentation de BlockingQueue: Quelles sont les différences entre SynchronousQueue et LinkedBlockingQueue

Je vois ces implémentations de BlockingQueue et ne peux pas comprendre les différences entre elles. Ma conclusion jusqu'à présent:

  1. Je n'aurai jamais besoin SynchronousQueue
  2. LinkedBlockingQueue assure FIFO, BlockingQueue doit être créé avec le paramètre true pour le rendre FIFO
  3. SynchronousQueue rompt la plupart des méthodes de collections (contient, taille, etc.)

Alors, quand ai-je besoin de SynchronousQueue? Les performances de cette implémentation sont-elles meilleures que LinkedBlockingQueue?

Pour le rendre plus compliqué ... pourquoi Executors.newCachedThreadPool utilise SynchronousQueue lorsque les autres (Executors.newSingleThreadExecutor et Executors.newFixedThreadPool) utilisent LinkedBlockingQueue ?

MODIFIER

La première question est résolue. Mais je ne comprends toujours pas pourquoi Executors.newCachedThreadPool utilise SynchronousQueue lorsque les autres (Executors.newSingleThreadExecutor et Executors.newFixedThreadPool) utilisent LinkedBlockingQueue?

Ce que j'obtiens, c'est qu'avec SynchronousQueue, le producteur sera bloqué s'il n'y a pas de thread libre. Mais comme le nombre de threads est pratiquement illimité (de nouveaux threads seront créés si nécessaire), cela ne se produira jamais. Alors, pourquoi devrait-il utiliser SynchronousQueue?

34
nanda

SynchronousQueue est un type de file d'attente très spécial - il implémente une approche de rendez-vous (le producteur attend que le consommateur soit prêt, le consommateur attend que le producteur soit prêt) derrière l'interface de Queue.

Par conséquent, vous pouvez en avoir besoin uniquement dans les cas spéciaux lorsque vous avez besoin de cette sémantique particulière, par exemple Single threading une tâche sans mettre en file d'attente d'autres requêtes .

Une autre raison d'utiliser SynchronousQueue est la performance. L'implémentation de SynchronousQueue semble être fortement optimisée, donc si vous n'avez besoin de rien de plus qu'un point de rendez-vous (comme dans le cas de Executors.newCachedThreadPool(), où les consommateurs sont créés "à la demande" , afin que les éléments de file d'attente ne s'accumulent pas), vous pouvez obtenir un gain de performances en utilisant SynchronousQueue.

Un test synthétique simple montre que dans un scénario simple producteur unique - consommateur unique sur un débit de machine à double cœur de SynchronousQueue est ~ 20 fois plus élevé que le débit de LinkedBlockingQueue et ArrayBlockingQueue avec file d'attente length = 1. Lorsque la longueur de la file d'attente est augmentée, leur débit augmente et atteint presque le débit de SynchronousQueue. Cela signifie que SynchronousQueue a une faible surcharge de synchronisation sur les machines multicœurs par rapport aux autres files d'attente. Mais encore une fois, cela n'a d'importance que dans des circonstances spécifiques lorsque vous avez besoin d'un point de rendez-vous déguisé en Queue.

MODIFIER:

Voici un test:

public class Test {
    static ExecutorService e = Executors.newFixedThreadPool(2);
    static int N = 1000000;

    public static void main(String[] args) throws Exception {    
        for (int i = 0; i < 10; i++) {
            int length = (i == 0) ? 1 : i * 5;
            System.out.print(length + "\t");
            System.out.print(doTest(new LinkedBlockingQueue<Integer>(length), N) + "\t");
            System.out.print(doTest(new ArrayBlockingQueue<Integer>(length), N) + "\t");
            System.out.print(doTest(new SynchronousQueue<Integer>(), N));
            System.out.println();
        }

        e.shutdown();
    }

    private static long doTest(final BlockingQueue<Integer> q, final int n) throws Exception {
        long t = System.nanoTime();

        e.submit(new Runnable() {
            public void run() {
                for (int i = 0; i < n; i++)
                    try { q.put(i); } catch (InterruptedException ex) {}
            }
        });    

        Long r = e.submit(new Callable<Long>() {
            public Long call() {
                long sum = 0;
                for (int i = 0; i < n; i++)
                    try { sum += q.take(); } catch (InterruptedException ex) {}
                return sum;
            }
        }).get();
        t = System.nanoTime() - t;

        return (long)(1000000000.0 * N / t); // Throughput, items/sec
    }
}    

Et voici un résultat sur ma machine:

enter image description here

54
axtavt

Actuellement, la valeur par défaut Executors (basée sur ThreadPoolExecutor) peut utiliser un ensemble de threads pré-créés de taille fixe et un BlockingQueue d'une certaine taille pour tout débordement ou créer des threads à une taille maximale si (et seulement si) cette file d'attente est pleine.

Cela conduit à des propriétés surprenantes. Par exemple, comme des threads supplémentaires ne sont créés qu'une fois la capacité de la file d'attente atteinte, l'utilisation d'un LinkedBlockingQueue (qui n'est pas limité) signifie que de nouveaux threads jamais seront créés, même si le pool actuel la taille est nulle. Si vous utilisez un ArrayBlockingQueue, les nouveaux threads ne sont créés que s'ils sont pleins et il y a une probabilité raisonnable que les travaux suivants soient rejetés si le pool n'a pas libéré d'espace d'ici là.

Un SynchronousQueue a une capacité nulle, donc un producteur bloque jusqu'à ce qu'un consommateur soit disponible ou qu'un thread soit créé. Cela signifie que malgré les chiffres impressionnants produits par @axtavt, un pool de threads mis en cache présente généralement les pires performances du point de vue du producteur.

Malheureusement, il n'y a actuellement pas de version de bibliothèque Nice d'une implémentation de compromis qui créera des threads pendant les rafales ou l'activité jusqu'à un maximum à partir d'un minimum faible. Vous avez soit une piscine évolutive, soit une piscine fixe. Nous en avons un en interne, mais il n'est pas encore prêt pour la consommation publique.

5
Jed Wesley-Smith

Le pool de threads de cache crée des threads à la demande. Il a besoin d'une file d'attente qui transmet la tâche à un consommateur en attente ou échoue. S'il n'y a pas de consommateur en attente, il crée un nouveau thread. SynchronousQueue ne contient pas d'élément, mais transmet l'élément ou échoue.

4
Peter Lawrey