web-dev-qa-db-fra.com

Quelle Java est la plus efficace pour les scénarios à un seul consommateur et à un seul consommateur)

Je travaille sur un système standard Java avec des délais critiques pour mes producteurs (1/100s de ms)).

J'ai un producteur qui place des trucs dans une file d'attente de blocage, et un seul consommateur qui les récupère plus tard et les jette dans un fichier. Le consommateur bloque lorsque les données ne sont pas disponibles.

Évidemment, la file d'attente de blocage est l'interface appropriée, mais quelle implémentation réelle dois-je choisir si je veux minimiser le coût pour le producteur? Je veux jouer le moins possible sur des choses comme le verrouillage et l'allocation lorsque je place des choses dans la file d'attente, et cela ne me dérange pas si le consommateur doit attendre beaucoup plus longtemps ou travailler beaucoup plus dur.

Y a-t-il une implémentation qui peut être plus rapide car je n'ai qu'un seul consommateur et un seul producteur?

39
Uri

Eh bien, il n'y a vraiment pas trop d'options. Permettez-moi de passer par les sous-classes répertoriées :

DelayQueue , LinkedBlockingDeque , PriorityBlockingQueue , et SynchronousQueue sont tous conçus pour des cas spéciaux nécessitant des fonctionnalités supplémentaires; ils n'ont pas de sens dans ce scénario.

Cela ne laisse que ArrayBlockingQueue et LinkedBlockingQueue . Si vous savez comment savoir si vous avez besoin d'un ArrayList ou d'un LinkedList, vous pouvez probablement répondre à celui-ci vous-même.

Notez que dans LinkedBlockingQueue, "les nœuds liés sont créés dynamiquement à chaque insertion"; cela pourrait avoir tendance à vous pousser vers ArrayBlockingQueue.

32
Michael Myers

Vous pouvez écrire un système sensible à la latence en Java mais vous devez faire attention aux allocations de mémoire, aux informations passant entre les threads et au verrouillage. Vous devriez écrire quelques tests simples pour voir combien de temps ces choses coûtent votre serveur (ils varient en fonction du système d'exploitation et de l'architecture de la mémoire)

Si vous faites cela, vous pouvez obtenir des horaires stables pour les opérations jusqu'à 99,99% du temps. Ce n'est pas correct en temps réel, mais cela peut être assez proche. Sur un système commercial, l'utilisation de Java au lieu des coûts C peut coûter 100 £/j, mais le coût de développement en C/C++ plutôt que Java est probable être beaucoup plus élevé que cela, par exemple en termes de flexibilité qu'il nous donne et du nombre de bugs qu'il enregistre.

Vous pouvez obtenir assez près la même gigue que vous verriez dans un programme C faisant la même chose. Certains appellent cela Java "C-like".

Malheureusement, il faut environ 8 micro-secondes pour passer un objet entre les threads en Java via un ArrayBlockingQueue sur les serveurs sur lesquels je travaille et je vous suggère de le tester sur vos serveurs. Un test simple consiste à passer le System.nanoTime () entre les threads et voyez combien de temps cela prend.

ArrayBlockingQueue a une "fonctionnalité" où il crée un objet sur chaque élément ajouté (bien qu'il n'y ait pas beaucoup de bonnes raisons de le faire) Donc, si vous trouvez une implémentation standard qui ne fait pas cela, faites le moi savoir.

14
Peter Lawrey

LinkedBlockingQueue aura O(1) coût d'insertion sauf en cas de retard d'allocation de mémoire. Un très grand ArrayBlockingQueue aura O(1) coût d'insertion à moins que le système ne soit bloqué en raison d'une récupération de place; il sera cependant bloqué lors de l'insertion à pleine capacité.

Même avec la récupération de place simultanée, je ne sais pas si vous devez écrire un système en temps réel dans un langage géré.

5
Andrew Duffy

Je suis d'accord avec la remarque d'Andrew Duffy selon laquelle la mise en œuvre d'un tel système limité dans le temps dans Java peut être une erreur. Quoi qu'il en soit, en supposant que vous êtes marié à la JVM, et que vous ne vous attendez pas à ce que cette file d'attente être plein (c.-à-d. que le consommateur peut gérer la charge), je pense que vous pourriez être mieux servi par une implémentation personnalisée, similaire à une ArrayBlockingQueue mais réduite/optimisée pour le scénario de producteur/consommateur unique. En particulier, j'aime l'idée de tourner du côté du producteur pour attendre de l'espace, plutôt que de bloquer.

J'ai fait référence à Java.concurrent sources pour des conseils, et rédigé cet algorithme ...

Cela me semble assez bon, mais ce n'est pas testé et peut ne pas être plus rapide dans la pratique (probablement pas révolutionnaire, mais je l'ai fait moi-même :-). Je me suis bien amusé avec ça, de toute façon ... Pouvez-vous trouver un défaut?

Pseudocode:

private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final E[] buffer;
private int head = 0
private int tail = 0;

public void put(E e) {
  if (e == null) throw NullPointerException();

  while (buffer[tail] != null) {
    // spin, wait for space...  hurry up, consumer!
    // open Q: would a tight/empty loop be superior here?
    Thread.sleep(1);
  }

  buffer[tail] = e;
  tail = (tail + 1) % buffer.length;

  if (count.getAndIncrement() == 0)  {
    sync(takeLock) { // this is pseudocode -- should be lock/try/finally/unlock
      notEmpty.signal();
    }
  }
}

public E take() {
  sync(takeLock) {
    while (count.get() == 0) notEmpty.await();
  }
  E item = buffer[head];
  buffer[head] = null;
  count.decrement();
  head = (head + 1) % buffer.length;

  return item;
}
4
joshng

Si les délais sont si serrés, vous devrez probablement effectuer une analyse comparative approfondie sur exactement le même matériel pour déterminer le meilleur ajustement.

Si je devais deviner, j'irais avec ArrayBlockingQueue parce que les collections basées sur des tableaux ont tendance à avoir une belle localité de référence.

3
Hank Gay

Vous pouvez étudier l'utilisation de LinkedTransferQueue , qui implémente la "double file d'attente avec du mou" et est bonne pour faire correspondre les producteurs et les consommateurs. Voir ci-dessous pour plus de détails Java 7 TransferQueue , Dual Synchronous Queues (.pdf) and LinkedTransferQueue source

3
dvvrt

ArrayBlockingQueue sera probablement plus rapide car vous n'avez pas besoin de créer de nœuds de lien lors de l'insertion. Notez, cependant, que ArrayBlockingQueue est d'une longueur fixe, si vous voulez que votre file d'attente s'agrandisse arbitrairement (au moins à Integer.MAX_INT), vous devrez utiliser un LinkedBlockingQueue (sauf si vous voulez allouer un tableau d'éléments Integer.MAX_INT) .

2
Victor