JavaRush /Blogue Java /Random-PT /Gerenciamento de fluxo. A palavra-chave volátil e o métod...

Gerenciamento de fluxo. A palavra-chave volátil e o método yield()

Publicado no grupo Random-PT
Olá! Continuamos estudando multithreading e hoje conheceremos uma nova palavra-chave - volátil e o método yield(). Vamos descobrir o que é :)

Palavra-chave volátil

Ao criar aplicativos multithread, podemos enfrentar dois problemas sérios. Primeiramente, durante a operação de uma aplicação multithread, diferentes threads podem armazenar em cache os valores das variáveis ​​(falaremos mais sobre isso na palestra “Usando voláteis” ). É possível que um thread tenha alterado o valor de uma variável, mas o segundo não tenha visto essa alteração porque estava trabalhando com sua própria cópia em cache da variável. Naturalmente, as consequências podem ser graves. Imagine que não se trata apenas de uma espécie de “variável”, mas, por exemplo, do saldo do seu cartão bancário, que de repente começou a saltar aleatoriamente para frente e para trás :) Não é muito agradável, certo? Em segundo lugar, em Java, operações de leitura e gravação em campos de todos os tipos, exceto longe doublesão atômicos. O que é atomicidade? Bem, por exemplo, se você alterar o valor de uma variável em um thread inte em outro thread você ler o valor dessa variável, você obterá seu valor antigo ou um novo - aquele que resultou após a mudança em tópico 1. Nenhuma “opção intermediária” aparecerá lá. Talvez. No entanto, isso não funciona com longe . doublePor que? Porque é multiplataforma. Você se lembra de como dissemos nos primeiros níveis que o princípio Java é “escrito uma vez, funciona em qualquer lugar”? Isso é multiplataforma. Ou seja, uma aplicação Java roda em plataformas completamente diferentes. Por exemplo, em sistemas operacionais Windows, diferentes versões de Linux ou MacOS e em todos os lugares este aplicativo funcionará de forma estável. longe double- as primitivas mais “pesadas” em Java: pesam 64 bits. E algumas plataformas de 32 bits simplesmente não implementam a atomicidade de leitura e gravação 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, depois outros 32. Conseqüentemente, nesses casos pode surgir um problema. Um thread grava algum valor de 64 bits em uma variávelХ, e ele faz isso “em duas etapas”. Ao mesmo tempo, o segundo thread tenta ler o valor desta variável, e faz isso bem no meio, quando os primeiros 32 bits já foram escritos, mas os segundos ainda não foram escritos. Como resultado, ele lê um valor intermediário incorreto e ocorre um erro. Por exemplo, se em tal plataforma tentarmos escrever um número em uma variável - 9223372036854775809 - ela ocupará 64 bits. Na forma binária, ficará assim: 10000000000000000000000000000000000000000000000000000000001 O primeiro thread começará a escrever esse número em uma variável e primeiro escreverá os primeiros 32 bits: 10000000000000000000000 0000 00000 e depois o segundo 32: 0000000000000000000000000001 E um segundo fio pode se encaixar nessa lacuna e leia o valor intermediário da variável - 1000000000000000000000000000000, os primeiros 32 bits que já foram escritos. No sistema decimal, esse número é igual a 2147483648. Ou seja, queríamos apenas escrever o número 9223372036854775809 em uma variável, mas devido ao fato dessa operação em algumas plataformas não ser atômica, obtivemos o número “esquerdo” 2147483648 , do qual não precisamos, do nada e não se sabe como isso afetará o funcionamento do programa. O segundo thread simplesmente leu o valor da variável antes de finalmente ser escrita, ou seja, viu os primeiros 32 bits, mas não os segundos 32 bits. Esses problemas, é claro, não surgiram ontem e em Java eles são resolvidos usando apenas uma palavra-chave - volátil . Se declararmos alguma variável em nosso programa com a palavra volátil...
public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…significa que:
  1. Sempre será lido e escrito atomicamente. Mesmo que seja de 64 bits doubleou long.
  2. A máquina Java não irá armazená-lo em cache. Portanto, a situação em que 10 threads trabalham com suas cópias locais é excluída.
É assim que dois problemas gravíssimos se resolvem em uma palavra :)

método rendimento()

Já vimos muitos métodos da classe Thread, mas há um importante que será novo para você. Este é o método yield() . Traduzido do inglês como “ceder”. E é exatamente isso que o método faz! Gerenciamento de fluxo.  A palavra-chave volátil e o método yield() - 2Quando chamamos o método yield em um thread, ele na verdade diz para outros threads: “Ok, pessoal, não estou com muita pressa, então se for importante para algum de vocês obter tempo de CPU, aproveitem, estou não urgente." Aqui está um exemplo simples de como funciona:
public class ThreadExample extends Thread {

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

   public void run() {

       System.out.println(Thread.currentThread().getName() + "give way 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 lançamos sequencialmente três threads - Thread-0, Thread-1e Thread-2. Thread-0começa primeiro e imediatamente dá lugar a outros. Depois disso Thread-1, ele começa e também cede. Depois disso, começa Thread-2, que também é inferior. Não temos mais threads, e depois que Thread-2o último cedeu seu lugar, o agendador de threads olha: “Então, não há mais threads novos, quem temos na fila? Quem foi o último a desistir do seu lugar antes Thread-2? Eu acho que foi Thread-1? Ok, então deixe ser feito. Thread-1faz seu trabalho até o final, após o qual o agendador de threads continua a coordenar: “Ok, Thread-1 foi concluído. Temos mais alguém na fila?" Há Thread-0 na fila: ele cedeu seu lugar imediatamente antes do Thread-1. Agora o assunto chegou até ele e ele está sendo levado até o fim. Após isso o escalonador finaliza a coordenação das threads: “Ok, Thread-2, você deu lugar para outras threads, todas já funcionaram. Você foi o último a ceder, então agora é a sua vez.” Depois disso, o Thread-2 é executado até a conclusão. A saída do console ficará assim: Thread-0 dá lugar a outros Thread-1 dá lugar a outros Thread-2 dá lugar a outros Thread-1 terminou de ser executado. Thread-0 terminou a execução. Thread-2 terminou a execução. O agendador de threads pode, é claro, executar threads em uma ordem diferente (por exemplo, 2-1-0 em vez de 0-1-2), mas o princípio é o mesmo.

Regras do que acontece antes

A última coisa que abordaremos hoje são os princípios do “ acontece antes ”. Como você já sabe, em Java, a maior parte do trabalho de alocação de tempo e recursos aos threads para completar suas tarefas é feita pelo agendador de threads. Além disso, você já viu mais de uma vez como os threads são executados em ordem arbitrária e, na maioria das vezes, é impossível prever isso. E em geral, depois da programação “sequencial” que fizemos antes, o multithreading parece uma coisa aleatória. Como você já viu, o progresso de um programa multithread pode ser controlado usando todo um conjunto de métodos. Mas, além disso, no multithreading Java existe outra “ilha de estabilidade” - 4 regras chamadas “ acontece antes ”. Literalmente do inglês, isso é traduzido como “acontece antes” ou “acontece antes”. O significado destas regras é bastante simples de entender. Imagine que temos dois threads - Ae B. Cada um desses threads pode executar operações 1e 2. E quando em cada uma das regras dizemos “ A acontece antes de B ”, isso significa que todas as alterações feitas pelo thread Aantes da operação 1e as alterações que esta operação implicou são visíveis para o thread Bno momento em que a operação é executada 2e após a operação ser realizada. Cada uma dessas regras garante que ao escrever um programa multithread, alguns eventos acontecerão antes de outros 100% das vezes, e que o thread Bno momento da operação 2estará sempre ciente das alterações que o thread Аfez durante a operação 1. Vamos dar uma olhada neles.

Regra 1.

A liberação de um mutex acontece antes de outro thread adquirir o mesmo monitor. Bem, tudo parece claro aqui. Se o mutex de um objeto ou classe for adquirido por um thread, por exemplo, um thread А, outro thread (thread B) não poderá adquiri-lo ao mesmo tempo. Você precisa esperar até que o mutex seja liberado.

Regra 2.

Thread.start() Isso acontece antes do método Thread.run(). Nada complicado também. Você já sabe: para que o código dentro do método comece a ser executado run(), você precisa chamar o método na thread start(). É dele, e não o método em si run()! Esta regra garante que Thread.start()os valores de todas as variáveis ​​definidas antes da execução ficarão visíveis dentro do método que iniciou a execução run().

Regra 3.

A conclusão do método run() acontece antes do método exit join(). Voltemos aos nossos dois fluxos - Аe B. Chamamos o método join()de tal forma que o thread Bdeve esperar até a conclusão Aantes de realizar seu trabalho. Isso significa que o método run()do objeto A certamente será executado até o fim. E todas as alterações nos dados que ocorrem no método run()thread Aficarão completamente visíveis no thread Bquando ele aguardar a conclusão Ae começar a funcionar sozinho.

Regra 4.

A gravação em uma variável volátil acontece antes da leitura da mesma variável. Ao usar a palavra-chave volátil, obteremos, de fato, sempre o valor atual. Mesmo no caso de longe double, cujos problemas foram discutidos anteriormente. Como você já entendeu, as alterações feitas em alguns threads nem sempre são visíveis para outros threads. Mas, é claro, muitas vezes há situações em que esse comportamento do programa não nos agrada. Digamos que atribuímos um valor a uma variável em um thread A:
int z;.

z= 555;
Se nosso thread Bimprimisse o valor de uma variável zno console, ele poderia facilmente imprimir 0 porque não sabe o valor atribuído a ela. Portanto, a Regra 4 nos garante: se você declarar uma variável zcomo volátil, as alterações em seus valores em um thread sempre serão visíveis em outro thread. Se adicionarmos a palavra volátil ao código anterior...
volatile int z;.

z= 555;
...a situação em que o fluxo Bproduzirá 0 no console é excluída. A gravação em variáveis ​​voláteis ocorre antes da leitura delas.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION