Por que eu devo ler este artigo:

Este artigo apresenta os principais recursos da linguagem Java para o desenvolvimento de aplicações com linhas de execução concorrentes, abrangendo sincronização de sessões críticas, criação de linhas de execução adicionais com threads, o uso de classes auxiliares, entre outros.

Tais recursos são acessíveis sob uma sintaxe simplificada e moderna, sem perder em eficiência nem legibilidade.

É apresentada também uma aplicação exemplo multi-thread que utiliza muitos dos recursos de concorrência, incluindo a implementação de semáforos disponibilizada pela linguagem Java. Em aplicações práticas, a correta sincronização de programas concorrentes pode evitar problemas sérios e difíceis de serem detectados.

Para exemplificar a aplicação de soluções possíveis para problemas de sincronização, é proposta uma aplicação exemplo baseada numa interface gráfica que utiliza tais funcionalidades para garantir a sincronia no acesso a recursos compartilhados.

Vamos abordar nesse artigo as principais classes e comandos da linguagem Java para se trabalhar com linhas de execução adicionais pertencentes ao mesmo programa, que são conhecidas como threads.

Cada aplicação Java já contém um threads por padrão, que é o thread principal da própria aplicação, porém outras linhas de execução adicionais podem ser criadas para realizar tarefas simultâneas.

Um exemplo de aplicação multi-thread clássico é um servidor de chat, onde vários clientes podem se conectar a ele usando sockets, causando a realização de diversas tarefas simultaneamente, como criação de novas conexões, fechamento de conexões existentes, gerenciamento de interface gráfica, realização de backup, envio e recebimento de mensagens para cada cliente e outros.

Usando threads, evitamos o caso de paralisar o servidor toda vez que uma dessas requisições precisar ser atendida, pois cada uma dessas tarefas é realizada ao mesmo tempo.

No dia a dia é comum usar aplicações que envolvam threads, como aplicações com interfaces gráficas (GUIs) e aplicações de comunicação. Em tais aplicações são executadas diversas tarefas simultaneamente por linhas de execução paralelas, onde o sistema operacional designa threads aos processadores durante intervalos de tempo que ele mesmo determina.

Felizmente, Java oferece recursos de alto nível que escondem a verdadeira complexidade do tratamento de threads no nível do sistema operacional e do hardware, permitindo ao desenvolvedor se focar no desenvolvimento da correta sincronização do uso de recursos compartilhados por threads através do uso de comandos simples e eficazes.

Um problema que pode prejudicar aplicações multi-thread se não tratado corretamente é a sincronização do acesso a recursos compartilhados, mais comumente caracterizados na forma de variáveis e objetos.

Portanto, em muitas vezes, organizações são prejudicadas por utilizarem software que contém falhas no controle de acesso a recursos compartilhados, levando a problemas como o caso da aplicação tentar criar registros no banco de dados com violação de unicidade de chaves únicas, perda de dados, erros esporádicos na aplicação, lentidão excessiva do sistema, entre outros.

Na maioria das linguagens de programação, não é uma atividade trivial desenvolver um programa multi-thread que apresenta sincronização eficiente de acesso a recursos compartilhados por tais threads, porém, na linguagem Java, são disponibilizadas uma série de classes, comandos e métodos a fim de facilitar o controle de acesso a tais recursos, liberando o programador de precisar vincular fortemente a lógica da aplicação ao controle de threads, aumentando a legibilidade do código-fonte, além de sua eficiência.

Disputa de sessões críticas

Um problema muito comum que pode ocorrer na programação concorrente é a atualização de uma variável compartilhada entre vários threads. Na Listagem 1 considere o trecho de código apresentado como sendo uma sessão crítica, ou seja, como sendo um trecho de código com variáveis que são visíveis e acessíveis para mais de um threads.

Listagem 1. Exemplo simples de sessão crítica.

x = x + y;
y = x + y + 1;

Se considerarmos que a sessão crítica na listagem é executada somente por um único thread, sempre será obtido o resultado correto ao final da sessão crítica. Como exemplo, levando-se em conta que x e y iniciam com o valor 1, o resultado será x=2 e y=4 ao final da linha 2.

Além disso, podemos considerar outro exemplo em que dois threads distintos, (a) e (b), estão a ponto de executar a sessão crítica e o sistema operacional, praticamente ao mesmo tempo, concede ao pedido de ambos os threads para executar essa sessão crítica.

Nessa situação, levando-se em conta o caso de haver um único processador, enquanto um thread estiver executando a sessão crítica, ele pode perder o uso do processador para o outro threads, podendo assim levar a resultados finais incorretos, já que ambos os threads compartilham ambas as variáveis.

O mesmo problema ocorre em computadores com múltiplos processadores ou núcleos, pois não se pode garantir a ordem de intercalação na execução das operações de threads concorrentes. Considere o caso ótimo em que o threads (a) ganha acesso à sessão crítica e a completa e logo em seguida o threads (b) ganha acesso à sessão crítica e a completa, como na Listagem 2.

Listagem 2. Exemplo de sincronização de acesso à sessão crítica (por coincidência).

(a)  x = x + y;         // (x=2, y=1)
(a)  y = x + y + 1;     // (x=2, y=4)  : resultado para (a)
(b)  x = x + y;         // (x=6, y=4)
(b)  y = x + y + 1;     // (x=6, y=11) : resultado para (b)

Nessa listagem, podemos verificar que o threads (a) executou a sessão crítica e obteve o resultado x=2 e y=4 (linha 2), e o threads (b) executou o mesmo código e obteve o resultado x=6 e y=11 (linha 4), que são os resultados corretos para cada thread.

Considere agora a possibilidade em que o sistema operacional realizou a intercalação das mesmas operações em uma ordem diferente, como exemplificado na Listagem 3.

Listagem 3. Exemplo de acesso à sessão crítica com falha de sincronização.

(a)  x = x + y;         // (x=2, y=1)
(b)  x = x + y;         // (x=3, y=1) : resultado para (a)
(b)  y = x + y + 1;     // (x=3, y=5)
(a)  y = x + y + 1;     // (x=3, y=9) : resultado para (b)

Como podemos verificar, o thread(a) executou a sessão crítica e obteve o resultado x=3 e y=9 (linha 4), e o threads (b) executou o mesmo código e obteve o resultado x=6 e y=5 (linha 3), diferindo dos resultados obtidos na Listagem 2.

Uma vez que o sistema operacional não tem como garantir a ordem de execução das operações intercaladas, confirmamos que a ordem de execução das operações pelo sistema operacional é importante para definir o resultado final das operações.

Para evitar tal problema, precisamos de mecanismos que nos façam garantir a execução sincronizada, como na Listagem 2, que leva a resultados corretos, onde um threads executa a sessão crítica por vez. Tal situação é comumente referenciada como “disputa” (race), momento em que dois threads disputam uma sessão crítica, e a solução é fazer com que somente um threads acesse por vez a sessão crítica, ou seja, garantir a exclusão mútua. Se o outro tentar acessar a sessão crítica nesse intervalo de tempo, ficará bloqueado até o primeiro thread terminar.

O referido problema não ocorre somente com variáveis simples, mas sim com todo o tipo de recurso computacional, como com vetores, sockets, impressoras, recursos do sistema operacional e outros.

Felizmente, Java oferece comandos simples para realizar a sincronização de sessões críticas, como o comando synchronized e suas várias aplicações. ...

Quer ler esse conteúdo completo? Tenha acesso completo