Perguntas da entrevista sobre multithreading Java
Um processo é um ambiente de execução independente, que pode ser considerado um programa ou um aplicativo. Um thread é uma tarefa executada em um processo. O Java Runtime Environment é um processo único que contém diferentes classes e programas. Threads podem ser chamados de processos leves. Threads requerem menos recursos para criar e residir em um processo e podem compartilhar recursos dentro do processo.
Em um programa multithread, vários threads são executados simultaneamente para melhorar a eficiência do programa. A CPU não entrará em estado inativo porque um thread precisa esperar por recursos. Vários threads compartilham memória heap, portanto, é melhor criar vários threads para executar algumas tarefas do que criar vários processos. Por exemplo, os Servlets são melhores que o CGI porque os Servlets suportam multithreading, enquanto o CGI não.
Quando criamos um thread em um programa Java, ele é chamado de thread do usuário. Um thread daemon é um thread executado em segundo plano e não impede o encerramento da JVM. Quando nenhum thread de usuário está em execução, a JVM fecha o programa e sai. Threads filhos criados por um thread daemon ainda são threads daemon.
Existem duas maneiras de criar um thread: uma é implementar a interface Runnable e, em seguida, passá-la para o construtor Thread para criar um objeto Thread, a outra é herdar diretamente a classe Thread; Se quiser saber mais, leia este artigo sobre como criar threads em Java.
Quando criamos um novo thread em um programa Java, seu status é Novo. Quando chamamos o método start() do thread, o status é alterado para Runnable. O agendador de threads aloca tempo de CPU para threads no conjunto de threads executáveis e altera seu status para Em execução. Outros estados de thread incluem Aguardando, Bloqueado e Morto. Leia este artigo para saber mais sobre o ciclo de vida do thread.
Claro, mas se chamarmos o método run() do Thread, ele se comportará como um método normal. Para executar nosso código em um novo thread, devemos usar o método Thread.start().
Podemos usar o método Sleep() da classe Thread para pausar o thread por um período de tempo. Deve-se observar que isso não encerra o thread. Assim que o thread for despertado do modo de suspensão, o status do thread será alterado para Executável e será executado de acordo com a programação do thread.
Cada thread tem uma prioridade. De modo geral, os threads de alta prioridade terão prioridade durante a execução, mas isso depende da implementação do agendamento do thread, que depende do sistema operacional. Podemos definir a prioridade dos threads, mas isso não garante que os threads de alta prioridade serão executados antes dos threads de baixa prioridade. A prioridade do thread é uma variável int (de 1 a 10), 1 representa a prioridade mais baixa, 10 representa a prioridade mais alta.
O agendador de threads é um serviço do sistema operacional responsável por alocar tempo de CPU para threads no estado Executável. Depois de criarmos um thread e iniciá-lo, sua execução depende da implementação do agendador de threads. A divisão de tempo refere-se ao processo de alocação do tempo de CPU disponível para threads executáveis disponíveis. A alocação do tempo de CPU pode ser baseada na prioridade do thread ou no tempo que o thread espera. O agendamento de threads não é controlado pela máquina virtual Java, portanto é melhor que o aplicativo o controle (ou seja, não torne seu programa dependente da prioridade do thread).
A troca de contexto é o processo de armazenamento e restauração do estado da CPU, que permite que a execução do thread retome a execução a partir do ponto de interrupção. A troca de contexto é um recurso essencial de sistemas operacionais multitarefa e ambientes multithread.
Podemos usar o método joint() da classe Thread para garantir que todos os threads criados pelo programa terminem antes da saída do método main(). Aqui está um artigo sobre o método joint() da classe Thread.
Quando os recursos podem ser compartilhados entre threads, a comunicação entre threads é um meio importante de coordená-los. Os métodos wait()/notify()/notifyAll() na classe Object podem ser usados para comunicação entre threads sobre o status dos bloqueios de recursos. Clique aqui para saber mais sobre thread wait, notify e notifyAll.
Cada objeto em Java possui um bloqueio (monitor, que também pode ser um monitor), e métodos como wait() e notify() são usados para aguardar o bloqueio do objeto ou notificar outros threads de que o monitor do objeto está disponível. Não há bloqueios ou sincronizadores disponíveis para nenhum objeto em threads Java. É por isso que esses métodos fazem parte da classe Object para que cada classe em Java tenha métodos básicos para comunicação entre threads
Quando um thread precisa chamar o método wait() de um objeto, o thread deve possuir o bloqueio do objeto. Em seguida, ele liberará o bloqueio do objeto e entrará no estado de espera até que outros threads chamem o método notify() no objeto. Da mesma forma, quando um thread precisa chamar o método notify() do objeto, ele liberará o bloqueio do objeto para que outros threads em espera possam obter o bloqueio do objeto. Como todos esses métodos exigem que o thread mantenha o bloqueio do objeto, o que só pode ser conseguido por meio de sincronização, eles só podem ser chamados em métodos sincronizados ou blocos sincronizados.
Os métodos sleep() e yield() da classe Thread serão executados no thread em execução no momento. Portanto, não faz sentido chamar esses métodos em outros threads que estão aguardando. É por isso que esses métodos são estáticos. Eles podem funcionar no thread em execução no momento e evitar que os programadores pensem erroneamente que esses métodos podem ser chamados em outros threads que não estão em execução.
Há muitas maneiras de garantir a segurança do thread em Java - sincronização, usando classes simultâneas atômicas, implementando bloqueios simultâneos, usando a palavra-chave volátil, usando classes imutáveis e classes thread-safe. Você pode aprender mais no tutorial de segurança de thread.
Quando usamos a palavra-chave volátil para modificar uma variável, o thread irá ler a variável diretamente e não armazená-la em cache. Isso garante que as variáveis lidas pelo thread sejam consistentes com aquelas na memória.
Um bloco sincronizado é uma escolha melhor porque não bloqueia o objeto inteiro (é claro que você também pode bloquear o objeto inteiro). Os métodos sincronizados bloqueiam o objeto inteiro, mesmo se houver vários blocos sincronizados não relacionados na classe, o que geralmente faz com que eles parem de executar e precisem esperar para obter o bloqueio do objeto.
O thread pode ser definido como um thread daemon usando o método setDaemon(true) da classe Thread. Deve-se observar que este método precisa ser chamado antes de chamar o método start(), caso contrário, uma IllegalThreadStateException será lançada.
ThreadLocal é usado para criar variáveis locais de thread. Sabemos que todos os threads de um objeto compartilharão suas variáveis globais, portanto, essas variáveis não são seguras para threads. Mas quando não queremos usar a sincronização, podemos escolher variáveis ThreadLocal.
Cada thread terá suas próprias variáveis de Thread e eles podem usar os métodos get()/set() para obter seus valores padrão ou alterar seus valores dentro do thread. As instâncias ThreadLocal normalmente desejam que seu estado de thread associado seja propriedades estáticas privadas. No artigo de exemplo ThreadLocal você pode ver um pequeno programa sobre ThreadLocal.
ThreadGroup é uma classe cujo objetivo é fornecer informações sobre grupos de threads.
A API ThreadGroup é relativamente fraca e não fornece mais funções que Thread. Ele tem duas funções principais: uma é obter a lista de threads ativos no grupo de threads e a outra é definir o manipulador de exceções não capturadas (manipulador de exceções ncapturadas) para o thread; No entanto, em Java 1.5, a classe Thread também adicionou o método setUncaughtExceptionHandler(UncaughtExceptionHandler eh), portanto ThreadGroup está obsoleto e não é recomendado continuar a ser usado.
t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){ @Overridepublic void uncaughtException(Thread t, Throwable e) {System.out.println("exceção ocorreu:"+e.getMessage());} });
Um dump de thread é uma lista de threads ativos da JVM, que é muito útil para analisar gargalos e conflitos do sistema. Há muitas maneiras de obter dumps de thread - usando Profiler, comando Kill -3, ferramenta jstack, etc. Eu prefiro a ferramenta jstack porque é fácil de usar e vem com JDK. Por ser uma ferramenta baseada em terminal, podemos escrever alguns scripts para gerar periodicamente dumps de thread para análise. Leia este documento para saber mais sobre como gerar dumps de thread.
Deadlock refere-se a uma situação em que mais de dois threads são bloqueados para sempre. Essa situação requer pelo menos mais dois threads e mais de dois recursos.
Para analisar o impasse, precisamos observar o dump do thread do aplicativo Java. Precisamos descobrir quais threads estão com status BLOQUEADO e os recursos pelos quais estão aguardando. Cada recurso possui um id único, utilizando este id podemos descobrir quais threads já possuem seu bloqueio de objeto.
Evitar bloqueios aninhados, usar bloqueios apenas quando necessário e evitar esperas indefinidas são formas comuns de evitar conflitos. Leia este artigo para saber como analisar conflitos.
java.util.Timer é uma classe de ferramenta que pode ser usada para agendar um thread para execução em um horário específico no futuro. A classe Timer pode ser usada para agendar tarefas únicas ou periódicas.
java.util.TimerTask é uma classe abstrata que implementa a interface Runnable. Precisamos herdar esta classe para criar nossas próprias tarefas agendadas e usar o Timer para agendar sua execução.
Aqui estão exemplos sobre java Timer.
Um pool de threads gerencia um grupo de threads de trabalho e também inclui uma fila para colocar tarefas aguardando para serem executadas.
java.util.concurrent.Executors fornece uma implementação da interface java.util.concurrent.Executor para criar conjuntos de encadeamentos. O exemplo Thread Pool mostra como criar e usar um pool de threads ou leia o exemplo ScheduledThreadPoolExecutor para aprender como criar uma tarefa periódica.
Perguntas da entrevista sobre simultaneidade em Java
Uma operação atômica refere-se a uma unidade de tarefa de operação que não é afetada por outras operações. As operações atômicas são um meio necessário para evitar inconsistência de dados em um ambiente multithread.
int++ não é uma operação atômica, portanto, quando um thread lê seu valor e adiciona 1, outro thread pode ler o valor anterior, o que causará um erro.
Para resolver este problema, devemos garantir que a operação de aumento seja atômica. Antes do JDK1.5, poderíamos usar a tecnologia de sincronização para fazer isso. A partir do JDK 1.5, o pacote java.util.concurrent.atomic fornece classes de carregamento de tipo int e long que garantem automaticamente que suas operações são atômicas e não requerem o uso de sincronização. Você pode ler este artigo para aprender sobre as classes atômicas do Java.
A interface Lock fornece operações de bloqueio mais escaláveis do que métodos sincronizados e blocos sincronizados. Eles permitem estruturas mais flexíveis que podem ter propriedades completamente diferentes e podem suportar múltiplas classes relacionadas de objetos condicionais.
Suas vantagens são:
Leia mais sobre exemplos de bloqueio
A estrutura Executor foi introduzida em Java 5 com a interface java.util.concurrent.Executor. A estrutura Executor é uma estrutura para tarefas assíncronas que são chamadas, agendadas, executadas e controladas de acordo com um conjunto de estratégias de execução.
A criação ilimitada de threads pode causar excesso de memória do aplicativo. Portanto, criar um pool de threads é uma solução melhor porque o número de threads pode ser limitado e esses threads podem ser reciclados e reutilizados. É muito conveniente criar um pool de threads usando a estrutura Executors. Leia este artigo para aprender como criar um pool de threads usando a estrutura Executor.
As características de java.util.concurrent.BlockingQueue são: quando a fila estiver vazia, a operação de obtenção ou exclusão de elementos da fila será bloqueada, ou quando a fila estiver cheia, a operação de adição de elementos à fila será bloqueada .
A fila de bloqueio não aceita valores nulos. Quando você tenta adicionar um valor nulo à fila, uma NullPointerException será lançada.
As implementações de fila de bloqueio são thread-safe e todos os métodos de consulta são atômicos e usam bloqueios internos ou outras formas de controle de simultaneidade.
A interface BlockingQueue faz parte da estrutura de coleções Java e é usada principalmente para implementar o problema produtor-consumidor.
Leia este artigo para aprender como implementar o problema produtor-consumidor usando filas de bloqueio.
Java 5 introduziu a interface java.util.concurrent.Callable no pacote de simultaneidade, que é muito semelhante à interface Runnable, mas pode retornar um objeto ou lançar uma exceção.
A interface Callable usa genéricos para definir seu tipo de retorno. A classe Executors fornece alguns métodos úteis para executar tarefas dentro de Callable no pool de threads. Como a tarefa Callable é paralela, temos que aguardar o resultado que ela retorna. O objeto java.util.concurrent.Future resolve esse problema para nós. Depois que o pool de threads envia a tarefa Callable, um objeto Future é retornado. Usando-o, podemos saber o status da tarefa Callable e obter o resultado da execução retornado pelo Callable. Future fornece o método get() para que possamos esperar o término do Callable e obter os resultados de sua execução.
Leia este artigo para saber mais exemplos sobre Callable, Future.
FutureTask é uma implementação básica do Future, que podemos usar com Executors para processar tarefas assíncronas. Normalmente não precisamos usar a classe FutureTask, mas ela se torna muito útil quando planejamos substituir alguns métodos da interface Future e manter a implementação básica original. Podemos simplesmente herdar dele e substituir os métodos que precisamos. Leia o exemplo Java FutureTask para aprender como usá-lo.
As classes de coleção Java são resistentes a falhas, o que significa que quando a coleção é modificada e um thread usa um iterador para percorrer a coleção, o método next() do iterador lançará uma exceção ConcurrentModificationException.
Os contêineres simultâneos suportam travessia simultânea e atualizações simultâneas.
As classes principais são ConcurrentHashMap, CopyOnWriteArrayList e CopyOnWriteArraySet Leia este artigo para saber como evitar ConcurrentModificationException.
Os executores fornecem alguns métodos utilitários para as classes Executor, ExecutorService, ScheduledExecutorService, ThreadFactory e Callable.
Os executores podem ser usados para criar facilmente pools de threads.
Texto original: journaldev.com Tradução: ifeve Tradutor: Zheng Xudong