CodeGym /Blogue Java /Random-PT /Gerenciando threads. A palavra-chave volátil e o método y...
John Squirrels
Nível 41
San Francisco

Gerenciando threads. A palavra-chave volátil e o método yield()

Publicado no grupo Random-PT
Oi! Continuamos nosso estudo de multithreading. Hoje vamos conhecer a volatilepalavra-chave e o yield()método. Vamos mergulhar :)

A palavra-chave volátil

Ao criar aplicativos multithread, podemos nos deparar com dois problemas sérios. Primeiro, quando um aplicativo multithread está em execução, diferentes threads podem armazenar em cache os valores das variáveis ​​(já falamos sobre isso na lição intitulada 'Usando voláteis' ). Você pode ter a situação em que um thread altera o valor de uma variável, mas um segundo thread não vê a alteração, porque está trabalhando com sua cópia em cache da variável. Naturalmente, as consequências podem ser graves. Suponha que não seja apenas uma variável qualquer, mas sim o saldo da sua conta bancária, que de repente começa a pular aleatoriamente para cima e para baixo :) Isso não parece divertido, certo? Em segundo lugar, em Java, as operações para ler e escrever todos os tipos primitivos,longdouble, são atômicos. Bem, por exemplo, se você mudar o valor de uma intvariável em um thread, e em outro thread você ler o valor da variável, você obterá seu valor antigo ou o novo, ou seja, o valor que resultou da alteração no thread 1. Não há 'valores intermediários'. No entanto, isso não funciona com longs e doubles. Por que? Por causa do suporte multiplataforma. Lembra que nos níveis iniciais dissemos que o princípio orientador do Java é 'escreva uma vez, execute em qualquer lugar'? Isso significa suporte multiplataforma. Em outras palavras, um aplicativo Java é executado em todos os tipos de plataformas diferentes. Por exemplo, em sistemas operacionais Windows, diferentes versões do Linux ou MacOS. Ele será executado sem problemas em todos eles. Pesando em 64 bits,longdoublesão os primitivos 'mais pesados' em Java. E certas plataformas de 32 bits simplesmente não implementam leitura e gravação atômica de variáveis ​​de 64 bits. Tais variáveis ​​são lidas e escritas em duas operações. Primeiro, os primeiros 32 bits são gravados na variável e, em seguida, outros 32 bits são gravados. Como resultado, pode surgir um problema. Um thread grava algum valor de 64 bits em uma Xvariável e o faz em duas operações. Ao mesmo tempo, um segundo thread tenta ler o valor da variável e o faz entre essas duas operações — quando os primeiros 32 bits foram escritos, mas os segundos 32 bits não. Como resultado, ele lê um valor intermediário incorreto e temos um bug. Por exemplo, se em tal plataforma tentamos escrever o número para um 9223372036854775809 a uma variável, ocupará 64 bits. Na forma binária, fica assim: 1000000000000000000000000000000000000000000000000000000000000001 O primeiro thread começa a escrever o número na variável. Primeiro escreve os primeiros 32 bits (1000000000000000000000000000000) e depois os segundos 32 bits (00000000000000000000000000000001) E a segunda thread pode ficar presa entre essas operações, lendo o valor intermediário da variável (100000000000000000000000000000000), que são os primeiros 32 bits que já foram escritos. No sistema decimal, esse número é 2.147.483.648. Ou seja, queríamos apenas escrever o número 9223372036854775809 para uma variável, mas devido ao fato dessa operação não ser atômica em algumas plataformas, temos o número maligno 2.147.483.648, que surgiu do nada e terá um efeito desconhecido no programa. A segunda thread simplesmente lê o valor da variável antes que ela termine de ser escrita, ou seja, a thread viu os primeiros 32 bits, mas não os segundos 32 bits. Claro, esses problemas não surgiram ontem. Java os resolve com uma única palavra-chave: volatile. Se usarmos ovolatilepalavra-chave ao declarar alguma variável em nosso programa…

public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…significa que:
  1. Ele sempre será lido e escrito atomicamente. Mesmo que seja de 64 bits doubleou long.
  2. A máquina Java não o armazenará em cache. Portanto, você não terá uma situação em que 10 threads estejam trabalhando com suas próprias cópias locais.
Assim, resolvem-se dois problemas gravíssimos com apenas uma palavra :)

O método yield()

Já revisamos muitos dos Threadmétodos da classe, mas há um importante que será novidade para você. É o yield()método . E faz exatamente o que o nome indica! Gerenciando threads.  A palavra-chave volátil e o método yield() - 2Quando chamamos o yieldmétodo em um thread, ele realmente fala com os outros threads: 'Ei, pessoal. Não estou particularmente com pressa de ir a lugar nenhum, então, se for importante para algum de vocês obter tempo de processador, aproveite - posso esperar'. Aqui está um exemplo simples de como isso funciona:

public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + " yields its place to others");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
Criamos e iniciamos sequencialmente três threads: Thread-0, Thread-1, e Thread-2. Thread-0começa primeiro e imediatamente cede aos outros. Então Thread-1é iniciado e também rende. Então Thread-2é iniciado, que também rende. Não temos mais threads e, depois Thread-2de ceder seu lugar por último, o agendador de threads diz: 'Hmm, não há mais threads novos. Quem temos na fila? Quem cedeu seu lugar antes Thread-2? Parece que foi Thread-1. Ok, isso significa que vamos deixá-lo correr'. Thread-1conclui seu trabalho e, em seguida, o escalonador de encadeamento continua sua coordenação: 'Ok, Thread-1concluído. Temos mais alguém na fila?'. Thread-0 está na fila: cedeu seu lugar logo antesThread-1. Ele agora tem sua vez e é executado até a conclusão. Em seguida, o escalonador termina de coordenar os threads: 'Ok, Thread-2você cedeu a outros threads e agora todos estão prontos. Você foi o último a ceder, então agora é a sua vez'. Em seguida, Thread-2é executado até a conclusão. A saída do console será assim: Thread-0 cede seu lugar a outros Thread-1 cede seu lugar a outros Thread-2 cede seu lugar a outros Thread-1 concluiu a execução. Thread-0 terminou a execução. Thread-2 terminou de executar. Obviamente, o agendador de threads pode iniciar os threads em uma ordem diferente (por exemplo, 2-1-0 em vez de 0-1-2), mas o princípio permanece o mesmo.

Regras do que acontece antes

A última coisa que abordaremos hoje é o conceito de ' acontece antes '. Como você já sabe, em Java o escalonador de threads executa a maior parte do trabalho envolvido na alocação de tempo e recursos para que os threads executem suas tarefas. Você também viu repetidamente como os threads são executados em uma ordem aleatória que geralmente é impossível de prever. E, em geral, depois da programação 'seqüencial' que fizemos anteriormente, a programação multithread parece algo aleatório. Você já acredita que pode usar uma série de métodos para controlar o fluxo de um programa multithread. Mas o multithreading em Java tem mais um pilar — as 4 regras ' acontece antes '. Entender essas regras é bastante simples. Imagine que temos dois segmentos - AeB. Cada um desses threads pode executar operações 1e 2. Em cada regra, quando dizemos ' A acontece antes de B ', queremos dizer que todas as alterações feitas pela thread Aantes da operação 1e as alterações resultantes dessa operação são visíveis para a thread Bquando a operação 2é executada e posteriormente. Cada regra garante que, quando você escreve um programa multithread, certos eventos ocorrerão antes de outros 100% do tempo e que, no momento da operação, o 2thread Bsempre estará ciente das alterações que o thread Afez durante a operação 1. Vamos analisá-los.

Regra 1.

A liberação de um mutex ocorre antes que o mesmo monitor seja adquirido por outro thread. Acho que você entende tudo aqui. Se o mutex de um objeto ou classe for adquirido por um thread, por exemplo, por thread A, outro thread (thread B) não poderá adquiri-lo ao mesmo tempo. Ele deve esperar até que o mutex seja liberado.

Regra 2.

O Thread.start()método acontece antes Thread.run() . Novamente, nada difícil aqui. Você já sabe que para começar a executar o código dentro do run()método, você deve chamar o start()método na thread. Especificamente, o método start, não o run()método em si! Esta regra garante que os valores de todas as variáveis ​​definidas antes de Thread.start()serem chamadas estarão visíveis dentro do run()método assim que ele for iniciado.

Regra 3.

O fim do run()método acontece antes do retorno do join()método. Voltemos aos nossos dois tópicos: Ae B. Chamamos o join()método para que o encadeamento Btenha a garantia de aguardar a conclusão do encadeamento Aantes de fazer seu trabalho. Isso significa que run()é garantido que o método do objeto A será executado até o fim. E todas as alterações nos dados que ocorrem no run()método de encadeamento Atêm cem por cento de garantia de serem visíveis no encadeamento, Buma vez que ele é concluído, esperando que o encadeamento Atermine seu trabalho para que possa começar seu próprio trabalho.

Regra 4.

A gravação em uma volatilevariável ocorre antes da leitura dessa mesma variável. Quando usamos a volatilepalavra-chave, sempre obtemos o valor atual. Mesmo com um longou double(falamos anteriormente sobre problemas que podem acontecer aqui). Como você já entendeu, as alterações feitas em alguns threads nem sempre são visíveis para outros threads. Mas, é claro, há situações muito frequentes em que tal comportamento não nos convém. Suponha que atribuímos um valor a uma variável no thread A:

int z;

….

z = 555;
Se nosso Bthread deve exibir o valor da zvariável no console, ele pode facilmente exibir 0, porque não sabe sobre o valor atribuído. Mas a Regra 4 garante que, se declararmos a zvariável como volatile, as alterações em seu valor em um thread sempre serão visíveis em outro thread. Se adicionarmos a palavra volatileao código anterior...

volatile int z;

….

z = 555;
...então evitamos a situação em que o thread Bpode exibir 0. A gravação em volatilevariáveis ​​ocorre antes da leitura delas.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION