A programação concorrente sempre foi sinônimo de desafio para grande parte dos desenvolvedores. Nesse contexto, muitos consideram que a programação orientada a objetos e a programação concorrente, depois de muito estudo e prática, deixam de ser algo tão complicado. Para a primeira, certamente a afirmação é coerente, mas como veremos ao longo do artigo, para a segunda existem controvérsias.
Quando um desenvolvedor precisa de uma nova linha de execução concorrente (thread), o máximo que ele pode fazer é criar e solicitar a execução da thread. Essa solicitação é então enviada ao escalonador, agente de baixo nível, geralmente do sistema operacional, que é responsável por coordenar a execução das threads. Tal agente é o único que tem o poder de determinar quando uma thread irá para a CPU para executar suas tarefas e por quanto tempo permanecerá lá. Portanto, a execução de threads, por natureza, é algo imprevisível; não há como determinar de antemão quando uma thread irá executar e por quanto tempo. Essa imprevisibilidade aumenta consideravelmente a complexidade de sistemas que utilizam threads, tornando difícil seu teste e a assertividade de seu processamento.
Além disso, problemas como deadlocks (vide BOX 1), que fazem com que duas ou mais threads fiquem bloqueadas “eternamente”, e o aumento de contenção, situação em que muitas threads tentam acessar uma mesma área de memória simultaneamente e apenas uma delas consegue o acesso, podem levar a inconsistências ou queda de desempenho, se não evitados ou tratados adequadamente.
Condição em que uma thread bloqueia um recurso X e precisa de um recurso Y para finalizar. Contudo, o recurso Y também está bloqueado por outra thread, que aguarda pelo recurso X para executar seu processamento.
Assim, algo que parece benéfico pode, subitamente, se tornar prejudicial. No entanto, independentemente disso, threads são inevitáveis. Imagine um servidor web que atenda apenas a uma requisição por vez (monothread). Em termos de eficiência, o desempenho de tal servidor seria pobre para o usuário, pois em situações em que o servidor receber vários acessos simultâneos pode se formar uma fila de espera na qual apenas uma requisição será atendida por vez, pela única thread que ele possui.
Já um servidor multithreading possibilita o atendimento a diversas requisições simultaneamente, o que é muito interessante em termos de experiência para o usuário. Esse e outros benefícios de um software multithreading se estendem tanto aos usuários quanto ao próprio sistema. Como exemplo, considere um programa leitor e transmissor de e-mails, no qual é possível receber e-mails enquanto outro está sendo redigido pelo usuário, sem que uma ação precise esperar a outra para acontecer. Junto a isso, há também um melhor uso dos recursos de hardware, pois com mais threads é possível deixar o sistema realizando alguma tarefa que independe de uma ação do usuário, mas que seja útil para o mesmo, como apagar e-mails que estejam na lixeira por mais de um mês.
O uso de threads leva a um modelo de programação conhecido como programação concorrente. Esse modelo é diferente de programas monothread, pois visa lidar com situações que, claro, não acontecem quando se tem apenas uma thread em execução. Por exemplo, considere uma situação em que uma variável precisa ter seu valor somado a 1 por cada thread ativa em um programa a cada um segundo. Se as threads ativas fizerem essa soma simultaneamente, o valor dessa variável poderá ficar inconsistente, pois, como dito, não há certeza de quando uma thread irá executar e nem por quanto tempo. Assim, há chances de uma thread ler o valor da variável e ser interrompida pelo escalonador logo após essa leitura, a thread seguinte ler o mesmo valor da thread anterior e, consequentemente, as duas somarem 1 ao mesmo valor da variável, o que tornaria o resultado inconsistente. Note que em um programa monothread esse problema não aconteceria.
Para evitar o problema de inconsistência na soma do exemplo citado, alguma abordagem especial deve ser aplicada, como bloquear o acesso à variável que mantém a soma e conceder a apenas uma thread por vez o acesso. No entanto, as diferentes abordagens necessárias em ambientes multithreading não são simples de aplicar. O bloqueio à variável, por exemplo, é algo bem complicado de construir. Para se ter uma ideia, imagine: como evitar que duas thread obtenham o bloqueio simultaneamente?
Devido a isso, muitas linguagens oferecem recursos nativos e bibliotecas complementares a fim de facilitar a vida do desenvolvedor quando o mesmo precisa lidar com esses tipos de situações. Em Java, essa biblioteca é formada por todas as interfaces, classes, enums e demais itens contidos no pacote java.util.concurrent. Esse pacote contempla a implementação da JSR 166 – Concurrency Utilities, especificação da API para funcionalidades como bloqueios de acessos simultâneos (locks), sincronizadores de threads, controlador de pool de threads, coordenador de tarefas assíncronas, coleções concorrentes, variáveis atômicas, entre outras.
Adicionada na versão 5 do Java, essa biblioteca vem evoluindo bastante, e agora, com a versão 8, novas melhorias foram feitas. Entre as mais significativas estão as novas propostas para situações já atendidas na API, como um novo modelo de lock (bloqueio), uma nova forma de coordenar tarefas assíncronas e uma nova abordagem para variáveis atômicas.
A partir do que foi comentado, ao longo deste artigo serão apresentadas quais são essas novas abordagens oferecidas pela API e como elas se relacionam com o que já existia; isso tudo através de exemplos.
O problema com áreas de memória compartilhadas
Quando uma área de dados de um programa pode ser acessada por mais de uma thread ao mesmo tempo, essa área é chamada de região crítica. E quando muitas threads querem alterar simultaneamente uma região crítica, resultados imprevisíveis podem acontecer, o que não é algo desejável. Para evitar essa situação é necessário aplicar uma técnica chamada de exclusão mútua, solução que restringe o acesso à região crítica a apenas uma thread por vez, o que impede que dados sejam alterados ao mesmo tempo por diversas linhas de processamento e garante a consistência dos mesmos.
A linguagem Java, desde seu início, oferece um meio de viabilizar a exclusão mútua: a palavra reservada synchronized. Com a sintaxe synchronized(instance){…}, essa instrução bloqueia o parâmetro recebido através de instance para a thread que realizou a execução. Dessa maneira, o objeto fica preso à thread, ou seja, ela obtém o bloqueio (lock) do objeto, e enquanto não liberar esse lock (sair do bloco synchronized), todas as outras que desejarem acessar o objeto terão que esperar, gerando um efeito exclusivo e bloqueante. Exclusivo porque somente uma thread pode pegar para si o lock de um objeto e bloqueante porque as outras terão que esperar a liberação do objeto para ter acesso ao mesmo. Bloqueios que são exclusivos e bloqueantes são conhecidos como bloqueios pessimistas.
Contudo, o lock de um objeto obtido através da instrução synchronized é um recurso de baixo nível que não oferece muita flexibilidade ao desenvolvedor. Desse modo, se uma thread entra em um bloco synchronized, não há como permitir que outras também entrem em tal bloco baseado em alguma condição, ou seja, a primeira thread fará todas as outras esperarem. Ademais, não há como determinar o tempo máximo de espera para as threads que estão nessa condição, a fim de evitar que elas fiquem esperando indefinidamente.
Devido a essa “rigidez” da instrução synchronized que a API de concorrência do Java trouxe abstrações de alto nível para os bloqueios. Inicialmente representadas pela interface Lock e as implementações ReadWriteLock, ReentrantLock e ReentrantReadWriteLock, essas novas opções possuem um objetivo semelhante ao da instrução synchronized, mas com a adição de métodos como o tryLock(), que possui como um de seus parâmetros o tempo máximo que uma thread deve esperar para obter o bloqueio, evitando a espera por tempo indeterminado.
Como base para demonstração desses recursos, utilizaremos uma classe que representa uma conta corrente e que possui as operações de crédito, débito e leitura do saldo. Dada uma instância de conta, caso essa sofra, por exemplo, invocação das operações de crédito e débito por duas ou mais threads simultaneamente, há grandes chances de o valor do saldo ficar inconsistente. Para evitar que isso ocorra, é necessário garantir que não mais de uma thread por vez consiga executar qualquer uma das operações que alterem o va ...
Confira outros conteúdos:
Black November
Desconto exclusivo para as primeiras 200 matrículas!
Pagamento anual
12x no cartão
De: R$ 69,00
Por: R$ 59,00
Total: R$ 708,00
Garanta o desconto
- Formação FullStack Completa
- Carreira Front-end I e II, Algoritmo e Javascript, Back-end e Mobile
- +10.000 exercícios gamificados
- +50 projetos reais
- Comunidade com + 200 mil alunos
- Estude pelo Aplicativo (Android e iOS)
- Suporte online
- 12 meses de acesso
Pagamento recorrente
Cobrado mensalmente no cartão
De: R$ 79,00
Por: R$ 64,00 /mês
Total: R$ 768,00
Garanta o desconto
- Formação FullStack Completa
- Carreira Front-end I e II, Algoritmo e Javascript, Back-end e Mobile
- +10.000 exercícios gamificados
- +50 projetos reais
- Comunidade com + 200 mil alunos
- Estude pelo Aplicativo (Android e iOS)
- Suporte online
- Fidelidade de 12 meses
- Não compromete o limite do seu cartão
<Perguntas frequentes>
Nossos casos de sucesso
Eu sabia pouquíssimas coisas de programação antes de começar a estudar com vocês, fui me especializando em várias áreas e ferramentas que tinham na plataforma, e com essa bagagem consegui um estágio logo no início do meu primeiro período na faculdade.
Estudo aqui na Dev desde o meio do ano passado!
Nesse período a Dev me ajudou a crescer muito aqui no trampo.
Fui o primeiro desenvolvedor contratado pela minha
empresa. Hoje eu lidero um time de desenvolvimento!
Minha meta é continuar estudando e praticando para ser um
Full-Stack Dev!
Economizei 3 meses para assinar a plataforma e sendo sincero valeu muito a pena, pois a plataforma é bem intuitiva e muuuuito didática a metodologia de ensino. Sinto que estou EVOLUINDO a cada dia. Muito obrigado!
Nossa! Plataforma maravilhosa. To amando o curso de desenvolvimento front-end, tinha coisas que eu ainda não tinha visto. A didática é do jeito que qualquer pessoa consegue aprender. Sério, to apaixonado, adorando demais.
Adquiri o curso de vocês e logo percebi que são os melhores do Brasil. É um passo a passo incrível. Só não aprende quem não quer. Foi o melhor investimento da minha vida!
Foi um dos melhores investimentos que já fiz na vida e tenho aprendido bastante com a plataforma. Vocês estão fazendo parte da minha jornada nesse mundo da programação, irei assinar meu contrato como programador graças a plataforma.
Wanderson Oliveira
Comprei a assinatura tem uma semana, aprendi mais do que 4 meses estudando outros cursos. Exercícios práticos que não tem como não aprender, estão de parabéns!
Obrigado DevMedia, nunca presenciei uma plataforma de ensino tão presente na vida acadêmica de seus alunos, parabéns!
Eduardo Dorneles
Aprendi React na plataforma da DevMedia há cerca de 1 ano e meio... Hoje estou há 1 ano empregado trabalhando 100% com React!
Adauto Junior
Já fiz alguns cursos na área e nenhum é tão bom quanto o de vocês. Estou aprendendo muito, muito obrigado por existirem. Estão de parabéns... Espero um dia conseguir um emprego na área.
Utilizamos cookies para fornecer uma melhor experiência para nossos usuários, consulte nossa política de privacidade.