Implementações Seguras da Estrutura de Dados LIFO Seguras

Implementações Seguras da Estrutura de Dados LIFO Seguras

1. Introdução

Neste tutorial,we’ll discuss various options for Thread-safe LIFO Data structure implementations.

Na estrutura de dados LIFO, os elementos são inseridos e recuperados de acordo com o princípio Last-In-First-Out. Isso significa que o último elemento inserido é recuperado primeiro.

Em ciência da computação,stack é o termo usado para se referir a tal estrutura de dados.

Umstack é útil para lidar com alguns problemas interessantes, como avaliação de expressão, implementação de operações de desfazer, etc. Como ele pode ser usado em ambientes de execução simultâneos, talvez seja necessário torná-lo seguro para threads.

2. CompreendendoStacks

Basicamente, aStack must implement the following methods:

  1. push() - adiciona um elemento no topo

  2. pop() - buscar e remover o elemento superior

  3. peek() - busca o elemento sem removê-lo do contêiner subjacente

Conforme discutido antes, vamos supor que queremos um mecanismo de processamento de comando.

Nesse sistema, desfazer comandos executados é um recurso importante.

Em geral, todos os comandos são enviados para a pilha e a operação de desfazer pode ser simplesmente implementada:

  • Métodopop() para obter o último comando executado

  • chame o métodoundo() no objeto de comando exibido

3. Compreendendo a segurança da rosca emStacks

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

As condições de corrida, em poucas palavras, ocorrem quando a execução correta do código depende do tempo e da sequência dos encadeamentos. Isso acontece principalmente se mais de um encadeamento compartilhar a estrutura de dados e essa estrutura não for projetada para essa finalidade.

Vamos examinar um método abaixo de uma 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;
}

Para explicar a condição potencial de corrida no código acima, vamos assumir dois threads executando esse código, conforme indicado na sequência abaixo:

  • O primeiro thread executa a terceira linha: define o objeto de resultado com o elemento no índice ‘head '

  • O segundo thread executa a terceira linha: define o objeto de resultado com o elemento no índice ‘head '

  • O primeiro thread executa a quinta linha: redefine o índice 'head' para o próximo elemento no array de apoio

  • O segundo thread executa a quinta linha: redefine o índice 'head' para o próximo elemento no array de apoio

Opa! Agora, ambas as execuções retornariam o mesmo objeto de resultado

Para evitar tais condições de corrida, neste caso, um thread não deve executar a primeira linha até que o outro thread termine de redefinir o índice de 'cabeça' na quinta linha. Em outras palavras, acessar o elemento no índice ‘head 'e redefinir o índice‘ head' deve ocorrer atomicamente para um encadeamento.

Claramente, neste caso, a execução correta do código depende do tempo dos threads e, portanto, não é seguro para thread.

4. Pilhas Seguras de Segmento Usando Bloqueios

Nesta seção, discutiremos duas opções possíveis para implementações concretas de um thread-safestack. 

Em particular, vamos cobrir aStack de Java de umArrayDeque.  decorado com thread-safe

Ambos usamLocks para acesso mutuamente exclusivo.

4.1. Usando o JavaStack

Java Collections tem uma implementação legada paraStack thread-safe, com base emVector, que é basicamente uma variante sincronizada deArrayList.

No entanto, o próprio documento oficial sugere considerar o uso deArrayDeque. Portanto, não entraremos em muitos detalhes.

Embora o JavaStack seja thread-safe e simples de usar, há grandes desvantagens com essa classe:

  • Não possui suporte para configuração da capacidade inicial

  • Ele usa bloqueios para todas as operações. Isso pode prejudicar o desempenho de execuções de thread único.

4.2. UsandoArrayDeque

Using the Deque interface is the most convenient approach for LIFO data structures as it provides all the needed stack operations.ArrayDeque é uma dessas implementações concretas.  

Uma vez que não está usando bloqueios para as operações, as execuções de thread único funcionariam bem. Mas para execuções multithread, isso é problemático.

No entanto, podemos implementar um decorador de sincronização paraArrayDeque.. Embora tenha um desempenho semelhante à classeStack do Java Collection Framework, o importante problema da classeStack, a falta de configuração de capacidade inicial, foi resolvido.

Vamos dar uma olhada nesta aula:

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

Observe que nossa solução não implementaDeque por si só para simplificar, pois contém muitos mais métodos.

Além disso, Goiaba contémSynchronizedDeque which é uma implementação pronta para produção de umArrayDequeue. decorado

5. Pilhas seguras para thread sem bloqueio

ConcurrentLinkedDeque é uma implementação sem bloqueio da interfaceDeque. This implementation is completely thread-safe, pois usa umefficient lock-free algorithm.

As implementações sem bloqueio são imunes aos seguintes problemas, diferentemente dos baseados em bloqueio.

  • Priority inversion - Isso ocorre quando o encadeamento de baixa prioridade mantém o bloqueio necessário para um encadeamento de alta prioridade. Isso pode causar o bloqueio de alta prioridade

  • Deadlocks - Isso ocorre quando diferentes threads bloqueiam o mesmo conjunto de recursos em uma ordem diferente.

Além disso, as implementações sem bloqueio têm alguns recursos que os tornam perfeitos para uso em ambientes únicos e multiencadeados.

  • Para estruturas de dados não compartilhadas e parasingle-threaded access, performance would be at par with ArrayDeque

  • Para estruturas de dados compartilhadas, desempenhovaries according to the number of threads that access it simultaneously.

E em termos de usabilidade, não é diferente deArrayDeque, pois ambos implementam a interfaceDeque.

6. Conclusão

Neste artigo, discutimos a estruturastack data e seus benefícios em sistemas de design, como motor de processamento de comando e avaliadores de expressão.

Além disso, analisamos várias implementações de pilha na estrutura de coleções Java e discutimos suas nuances de desempenho e segurança de encadeamento.

Como de costume, exemplos de código podem ser encontradosover on GitHub.