Multithreading em Java

A Java Virtual Machine suporta computação paralela . Todos os cálculos podem ser executados no contexto de um ou mais threads. Podemos configurar facilmente o acesso ao mesmo recurso ou objeto para vários threads, bem como configurar um thread para executar um único bloco de código.

Qualquer desenvolvedor precisa sincronizar o trabalho com threads durante operações de leitura e gravação para recursos que possuem vários threads alocados para eles.

É importante que no momento de acessar o recurso você tenha dados atualizados para que outro thread possa alterá-lo e você obtenha as informações mais atualizadas. Mesmo que tomemos o exemplo de uma conta bancária, até que o dinheiro chegue a ela, você não pode usá-la, por isso é importante ter dados sempre atualizados. Java tem classes especiais para sincronizar e gerenciar threads.

Objetos de encadeamento

Tudo começa com o thread principal (principal), ou seja, pelo menos seu programa já possui um thread em execução. O thread principal pode criar outros threads usando Callable ou Runnable . A criação difere apenas no resultado de retorno, Runnable não retorna um resultado e não pode lançar uma exceção verificada. Portanto, você tem uma boa oportunidade de construir um trabalho eficiente com arquivos, mas isso é muito perigoso e você precisa ter cuidado.

Também é possível agendar a execução do thread em um núcleo de CPU separado. O sistema pode se mover facilmente entre as threads e executar uma thread específica com as configurações corretas: ou seja, a thread que lê os dados é executada primeiro, assim que tivermos os dados, depois passamos para a thread responsável pela validação, depois disso, passamos para o thread para executar alguma lógica de negócios e um novo thread os escreve de volta. Em tal situação, 4 threads estão processando dados por vez e tudo funcionará mais rápido do que um thread. Cada um desses fluxos é convertido em um fluxo de sistema operacional nativo, mas como ele será convertido depende da implementação da JVM.

A classe Thread é usada para criar e trabalhar com threads. Possui mecanismos de controle padrão, bem como abstratos, como classes e coleções de java.util.concurrent .

Sincronização de Threads em Java

A comunicação é fornecida pelo compartilhamento de acesso aos objetos. Isso é muito eficaz, mas ao mesmo tempo é muito fácil cometer erros ao trabalhar. Erros ocorrem em dois casos: interferência de encadeamento - quando outro encadeamento interfere no seu encadeamento e erros de consistência de memória - consistência de memória. Para resolver e prevenir esses erros, temos diferentes métodos de sincronização.

A sincronização de threads em Java é feita por monitores, este é um mecanismo de alto nível que permite que apenas um thread execute um bloco de código protegido pelo mesmo monitor por vez. O comportamento dos monitores é considerado em termos de bloqueios; um monitor - um bloqueio.

A sincronização tem vários pontos importantes aos quais você precisa prestar atenção. O primeiro ponto é a exclusão mútua - apenas um thread pode possuir o monitor, portanto, a sincronização no monitor implica que uma vez que um thread entra em um bloco sincronizado protegido pelo monitor, nenhum outro thread pode entrar no bloco protegido pelo monitor. primeiro thread sai do bloco sincronizado. Ou seja, vários threads não podem acessar o mesmo bloco sincronizado ao mesmo tempo.

Mas a sincronização não é apenas exclusão mútua. A sincronização garante que os dados gravados na memória antes ou dentro de um bloco sincronizado se tornem visíveis para outros threads sincronizados no mesmo monitor. Após sair do bloco, liberamos o monitor e outra thread pode agarrá-lo e começar a executar este bloco de código.

Quando um novo thread captura o monitor, ganhamos acesso e a capacidade de executar esse bloco de código e, nesse momento, as variáveis ​​serão carregadas da memória principal. Então podemos ver todas as entradas tornadas visíveis pela versão anterior do monitor.

Uma leitura/gravação em um campo é uma operação atômica se o campo for declarado volátil ou protegido por um bloqueio exclusivo adquirido antes de qualquer leitura/gravação. Mas se você ainda encontrar um erro, receberá um erro sobre reordenar (alterar a ordem, reordenar). Manifesta-se em programas multi-thread sincronizados incorretamente, onde um thread pode observar os efeitos produzidos por outros threads.

O efeito de exclusão mútua e sincronização de threads, ou seja, sua operação correta é obtida apenas inserindo um bloco ou método sincronizado que adquira implicitamente um bloqueio ou obtendo explicitamente um bloqueio. Falaremos sobre isso abaixo. Ambas as formas de trabalhar afetam sua memória e é importante não esquecer de trabalhar com variáveis ​​voláteis .

Campos voláteis em Java

Se uma variável estiver marcada como volátil , ela estará disponível globalmente. Isso significa que, se um thread acessar uma variável volátil , obterá seu valor antes de usar o valor do cache.

Uma gravação funciona como uma liberação de monitor e uma leitura funciona como uma captura de monitor. O acesso é realizado em uma relação do tipo “realizado antes”. Se você descobrir, tudo o que ficará visível para o thread A quando ele acessar uma variável volátil é a variável do thread B. Ou seja, você tem a garantia de não perder suas alterações de outros threads.

Variáveis ​​​​voláteis são atômicas, ou seja, ao ler tal variável, o mesmo efeito é usado ao obter um bloqueio - os dados na memória são declarados inválidos ou incorretos e o valor da variável volátil é novamente lido da memória . Ao gravar, o efeito na memória é usado, bem como ao liberar um bloqueio - um campo volátil é gravado na memória.

Java Simultâneo

Se você deseja criar um aplicativo supereficiente e multiencadeado, deve usar as classes da biblioteca JavaConcurrent , que estão no pacote java.util.concurrent .

A biblioteca é muito volumosa e possui diversas funcionalidades, então vamos dar uma olhada no que tem dentro e dividir em alguns módulos:

Java Simultâneo

Coleções simultâneas são um conjunto de coleções para trabalhar em um ambiente multiencadeado. Em vez do wrapper básico Collections.synchronizedList com bloqueio de acesso a toda a coleção, bloqueios são usados ​​em segmentos de dados ou algoritmos sem espera são usados ​​para ler dados em paralelo.

Filas - filas sem bloqueio e com bloqueio para trabalhar em um ambiente multiencadeado. As filas sem bloqueio concentram-se na velocidade e na operação sem bloqueio de threads. As filas de bloqueio são adequadas para o trabalho quando você precisa “desacelerar” os encadeamentos do produtor ou do consumidor . Por exemplo, em uma situação em que algumas das condições não são atendidas, a fila está vazia ou cheia ou não há Consumidor 'a livre.

Os sincronizadores são utilitários para sincronizar threads. Eles são uma arma poderosa na computação "paralela".

Executors é uma estrutura para criação mais conveniente e fácil de pools de threads, é fácil configurar o agendamento de tarefas assíncronas com a obtenção de resultados.

Os bloqueios são muitos mecanismos flexíveis de sincronização de encadeamento em comparação com o sincronizado básico , aguarde , notifique e notifiqueTodos .

Atomics são classes que podem suportar operações atômicas em primitivas e referências.