Java Concurrency

Aula
Estado
💡
  • Ações são implementadas dentro do método run da interface funcional Runnable(retorna void) ou do método call da interface funcional Callable (retorno genérico)
  • Uma instância da classe Thread recebe como parâmetros uma instância que implemente a interface Runnable; o processo é agendado para execução quando o método start() do novo objeto Thread é invocado
notion image
  • Como um processador pode ter muitas coisas rodando, o que o Java faz é alocar porções de tempo de CPU para executar as threads; com isso uma mesma thread pode trocar de core do CPU de acordo com novas tarefas que vão surgindo
    • uma thread pode ser interrompida a qualquer momento para dar lugar a outra thread
    • a ordem na qual ações são executadas é incerta; pode ocorrer um cenário onde um mesmo método, que esteja já agendado para execução, finalize depois de um método agendado posteriormente
      • notion image
  • Se o método main ou run retornarem, a thread é encerrada

Implementações de Threads

public class Lateral implements Runnable { public void run() {...} } Thread t = new Thread(new Lateral()); t.start();
Uma classe que implementa a Runnable
// a classe Thread implementa Runnable public class Lateral extends Thread { public void run() {...} } Thread t = new Lateral(); t.start();
Um classe que estende Thread sobrescrevendo o método run
Runnable r = () -> {...}; Thread t = new Thread(r); t.start();
Uma classe que implementa a interface funcional Runnable utilizando a sintaxe de Lambda Expression

Thread Life Cycle

💡
Uma Thread não começa a ser executada instantaneamente, o scheduler precisa primeiro alocar tempo de CPU para essa Thread
  • Uma thread pode estar nos estados:
    • NEW ainda não foi iniciada
    • RUNNABLE está sendo executada
    • BLOCKED está bloqueada e aguardando uma trava de monitorador ser liberada
    • WAITING está aguardando um sinal de outra thread
    • TIMED_WAITING está esperando um tempo específico de tempo
    • TERMINATED foi interrompida
Possíveis transições de estado
Possíveis transições de estado
  • É possível ver o estado "atual" da Thread com o método getState(); deve ser levado em consideração que o estado muda muito frequentemente, então o estado retornado pode não ser realmente o atual
  • Um Thread já iniciada, não pode ser iniciada novamente; uma mesma instância de Runnable pode ser executava múltiplas vezes simultaneamente, porém, cada uma deve ter a sua própria Thread
  • Transições para o estado RUNNABLE não são instantâneas, pois o scheduler deve alocar tempo de CPU para essa Thread

Interrupção de Thread

  • O método run de Runnable é responsável por tomar decisões de mudança de estado; isso quer dizer que o seu código em run deve ouvir por mudanças de estado e tomar ações correspondentes
    • isso quer dizer que seu método run decide o que irá acontecer quando o método interrupt() for invocado
  • Quando a thread está no estado RUNNABLE ela não precisa checar se recebeu um sinal de interrupção; porém, se estiver em WAITING ou TIMED_WAITING deve ter um catch para capturar um sinal de interrupção (InterruptedException)
notion image

Bloqueamento de Thread

  • É possível definir métodos ou blocos de código síncronos, utilizando a palavra reservada synchronized
  • Quando um trecho de código síncrono é acessado por uma Thread, ela bloqueia o acesso do escopo imediatamente superior ao trecho de código; esse escopo é chamado de objeto monitor
  • se um método estático for síncrono — como o método estático pertence a classe, não a uma instância — toda a classe a que esse método pertence, atuará como objeto monitor
notion image
  • se um método membro de uma classe for síncrono, todo acesso à instância que ele pertence será bloqueado até a Thread encerrar a execução desse método
  • se um trecho de código, dentro de um método, for síncrono, todo acesso a essa instância será bloqueado até a Thread encerrar a execução desse método
  • após uma Thread completar sua execução do trecho de código síncrono, ela libera o acesso para as outras Threads

Forçar uma Thread a esperar até ser notificada

  • O método wait da classe Object coloca uma Thread em estado de WAITING, até que o método notify ou notifyAll, desse mesmo objeto monitor — podendo ser uma instância ou uma classe, dependendo do tipo de código síncrono que foi executado — for invocado
  • O método notify de um objeto monitor escolhe, de forma aleatória, uma Thread que esteja aguardando esse objeto monitor para o estado RUNNABLE
  • O método notifyAll de um objeto monitor coloca todas Threads que estejam esperando esse objeto monitor no estado RUNNABLE
notion image

Propriedades de uma Thread

  • É possível definir um nome para uma Thread no seu construtor
  • Uma Thread tem um id único; getId() retorna ele
  • Uma Thread pode ser definida como um daemon ou user (padrão é user); só é possível definir o tipo da Thread antes dela ser iniciada
    • a JVM é finalizada quando as únicas Threads remanescentes são do tipo daemon (um processo de background)
  • Uma Thread pode esperar pela finalização de outra Thread (join)
    • ao invocar t.join() a thread que invocou esse método (podendo ser a thread padrão da main) interrompe sua execução até que a Thread t seja finalizada
  • Uma Thread pode ter uma prioridade; essa prioridade determina quanto de tempo do CPU será alocado para a Thread, mas não garante ordem de execução
    • dependendo do sistema onde a aplicação está sendo executada, pode ocorrer da prioridade ser ignorada

Executor

💡
Facilitam o controle de múltiplas threads
Existem diferentes objetos de configuração de ExecutorService's:
  • Fixed Thread Pool: reutiliza um número fixo de threads
  • Work Stealing Pool: mantém threads suficientes para manter um certo nível de paralelismo
  • Single Thread Executor: utiliza apenas uma thread
  • Cached Thread Pool: cria novas threads quando for necessário e reutiliza as existentes
  • Scheduled Thread Pool: agenda a execução de Runnable's periodicamente ou com um delay
  • Single Thread Scheduled Executor: agenda a execução de Runnable's com um delay em uma única Thread
  • Unconfigurable Executor Service: possibilita o congelamento das configurações de qualquer configuração anterior
notion image

Objeto Called

  • Ao ser chamado com o método submit(t) de algum ExecutorService retorna um objeto Future<T>
    • o objeto Future tem um método get(<qtd unidades de tempo>, <tipo da unidade de tempo>), ao chamar esse método o código aguarda um retorno de resultado até recebê-lo ou lançar um time out de acordo com o tempo definido nos parâmetros

Problemas de Lock

Alguns problemas surgem quando se tenta controlar a natureza aleatória das threads. Esse problemas dependem muito do hardware que executa a aplicação. Como eles dependem do ambiente, se torna absurdamente difícil encontra-los. É possível, ainda, que a aplicação não denuncie nenhum erro, mas que a aplicação apenas fique mais lenta ou ela simplesmente pare sua execução.
Para solucionar esse problema existe apenas uma solução: eliminar a dependência entre threads.

Starvation

Ocorre quando uma Thread está aguardando um recurso bloqueado por outra Thread que está ocupada.
notion image

Livelock

Ocorre quando uma thread T espera pela finalização de outra thread U, e a thread U espera pela finalização da thread T. Isso gera um loop onde nenhuma thread é finalizada.
notion image
notion image

Deadlock

Ocorre ao utilizar dois blocos síncronos, em duas threads, com a ordem inversa. Causando um loop onde ambos objetos monitores são bloqueados e nunca liberados.
 
💡
Uma Java Thread um caminho de execução e não implica paralelismo físico no CPU