Implémentations de la structure de données LIFO de Thread Safe

Implémentations de la structure de données LIFO de Thread Safe

1. introduction

Dans ce didacticiel,we’ll discuss various options for Thread-safe LIFO Data structure implementations.

Dans la structure de données LIFO, les éléments sont insérés et récupérés selon le principe du dernier entré, premier sorti. Cela signifie que le dernier élément inséré est récupéré en premier.

En informatique,stack est le terme utilisé pour désigner une telle structure de données.

Unstack est pratique pour traiter certains problèmes intéressants comme l'évaluation d'expressions, l'implémentation d'opérations d'annulation, etc. Comme il peut être utilisé dans des environnements d'exécution simultanés, il peut être nécessaire de le rendre thread-safe.

2. ComprendreStacks

Fondamentalement, unStack must implement the following methods:

  1. push() - ajoute un élément en haut

  2. pop() - récupère et supprime l'élément supérieur

  3. peek() - récupère l'élément sans le supprimer du conteneur sous-jacent

Comme indiqué précédemment, supposons que nous voulons un moteur de traitement de commandes.

Dans ce système, l'annulation des commandes exécutées est une caractéristique importante.

En général, toutes les commandes sont placées dans la pile, puis l'opération d'annulation peut être simplement implémentée:

  • Méthodepop() pour obtenir la dernière commande exécutée

  • appeler la méthodeundo() sur l'objet de commande popped

3. Comprendre la sécurité des threads enStacks

If a data structure is not thread-safe, when accessed concurrently, it might end up having race conditions.

En bref, les conditions de concurrence surviennent lorsque l'exécution correcte du code dépend du timing et de la séquence des threads. Cela se produit principalement si plusieurs threads partagent la structure de données et que cette structure n'est pas conçue à cet effet.

Examinons une méthode ci-dessous à partir d'une classe Java Collection,ArrayDeque:

public E pollFirst() {
    int h = head;
    E result = (E) elements[h];
    // ... other book-keeping operations removed, for simplicity
    head = (h + 1) & (elements.length - 1);
    return result;
}

Pour expliquer la situation de concurrence potentielle dans le code ci-dessus, supposons que deux threads exécutent ce code, comme indiqué dans la séquence ci-dessous:

  • Le premier thread exécute la troisième ligne: définit l'objet de résultat avec l'élément à l'index "head"

  • Le deuxième thread exécute la troisième ligne: définit l'objet de résultat avec l'élément à l'index "head"

  • Le premier thread exécute la cinquième ligne: réinitialise l'index "head" sur l'élément suivant dans le tableau de support

  • Le deuxième thread exécute la cinquième ligne: réinitialise l’index ‘head’ sur le prochain élément du tableau de support

Oops! Désormais, les deux exécutions renverraient le même objet de résultat

Pour éviter de telles conditions de concurrence, dans ce cas, un thread ne doit pas exécuter la première ligne tant que l’autre thread n’a pas fini de réinitialiser l’index «head» à la cinquième ligne. En d’autres termes, l’accès à l’élément situé dans l’index «head» et la réinitialisation de cet index doivent se faire de manière atomique pour un thread.

Clairement, dans ce cas, l’exécution correcte du code dépend de la synchronisation des threads et n’est donc pas sûre pour les threads.

4. Piles sans danger avec des verrous

Dans cette section, nous allons discuter de deux options possibles pour des implémentations concrètes d'unstack.  thread-safe

En particulier, nous aborderons le sable JavaStack un thread-safe décoréArrayDeque. 

Les deux utilisentLocks pour un accès mutuellement exclusif.

4.1. Utilisation de JavaStack

Java Collections a une implémentation héritée pour lesStack thread-safe, basée surVector qui est fondamentalement une variante synchronisée deArrayList.

Cependant, le document officiel lui-même suggère d'envisager d'utiliserArrayDeque. Par conséquent, nous n'entrerons pas trop dans les détails.

Bien que JavaStack soit thread-safe et simple à utiliser, il y a des inconvénients majeurs avec cette classe:

  • Il ne prend pas en charge la définition de la capacité initiale

  • Il utilise des verrous pour toutes les opérations. Cela pourrait nuire aux performances pour les exécutions à un seul thread.

4.2. Utilisation deArrayDeque

Using the Deque interface is the most convenient approach for LIFO data structures as it provides all the needed stack operations.ArrayDeque est une telle implémentation concrète.  

Comme il n'utilise pas de verrous pour les opérations, les exécutions à thread unique fonctionneraient très bien. Mais pour les exécutions multi-threadées, cela pose problème.

Cependant, nous pouvons implémenter un décorateur de synchronisation pourArrayDeque. Bien que cela fonctionne de la même manière que la classeStack de Java Collection Framework, le problème important de la classeStack, le manque de paramètre de capacité initial, est résolu.

Jetons un œil à ce cours:

public class DequeBasedSynchronizedStack {

    // Internal Deque which gets decorated for synchronization.
    private ArrayDeque dequeStore;

    public DequeBasedSynchronizedStack(int initialCapacity) {
        this.dequeStore = new ArrayDeque<>(initialCapacity);
    }

    public DequeBasedSynchronizedStack() {
        dequeStore = new ArrayDeque<>();
    }

    public synchronized T pop() {
        return this.dequeStore.pop();
    }

    public synchronized void push(T element) {
        this.dequeStore.push(element);
    }

    public synchronized T peek() {
        return this.dequeStore.peek();
    }

    public synchronized int size() {
        return this.dequeStore.size();
    }
}

Notez que notre solution n'implémente pasDeque lui-même par souci de simplicité, car elle contient beaucoup plus de méthodes.

De plus, Guava contientSynchronizedDeque w, qui est une implémentation prête pour la production d'unArrayDequeue. décoré

5. Piles fil-safe sans verrouillage

ConcurrentLinkedDeque est une implémentation sans verrouillage de l'interfaceDeque. This implementation is completely thread-safe car il utilise unefficient lock-free algorithm.

Les implémentations sans verrouillage sont immunisées contre les problèmes suivants, contrairement aux solutions basées sur le verrouillage.

  • Priority inversion - Cela se produit lorsque le thread de faible priorité détient le verrou requis par un thread de haute priorité. Cela pourrait provoquer le blocage du thread de haute priorité

  • Deadlocks - Cela se produit lorsque différents threads verrouillent le même ensemble de ressources dans un ordre différent.

De plus, les implémentations sans verrouillage possèdent certaines fonctionnalités qui les rendent idéales pour une utilisation dans des environnements à un ou plusieurs threads.

  • Pour les structures de données non partagées et poursingle-threaded access, performance would be at par with ArrayDeque

  • Pour les structures de données partagées, performancesvaries according to the number of threads that access it simultaneously.

Et en termes de convivialité, ce n'est pas différent deArrayDeque car les deux implémentent l'interfaceDeque.

6. Conclusion

Dans cet article, nous avons discuté de la structure de donnéesstack  et de ses avantages dans la conception de systèmes tels que le moteur de traitement de commandes et les évaluateurs d'expression.

Nous avons également analysé diverses implémentations de piles dans la structure de collections Java et examiné leurs nuances de performances et de sécurité des threads.

Comme d'habitude, des exemples de code peuvent être trouvésover on GitHub.