Multithreading
1. Conceitos relacionados
Simultaneidade e Paralelismo
Paralelo (paralelo): refere-se a várias tarefas de evento ocorrendo ao mesmo tempo (simultaneamente).
Simultaneidade : Refere-se a dois ou mais eventos que ocorrem dentro do mesmo pequeno período de tempo .A execução simultânea de programas pode fazer uso total dos recursos da CPU em condições limitadas.
CPU single-core: apenas simultânea
CPU multi-core: paralela + simultânea
Tópicos e Processos
-
Programa : Para concluir uma determinada tarefa e função, escolha um conjunto de instruções escritas em uma linguagem de programação.
-
Software : 1 ou mais aplicativos + materiais relacionados e arquivos de recursos constituem um sistema de software.
-
Um processo é uma descrição do processo em execução (criação-execução-morte) de um programa.O sistema cria um processo para cada programa em execução e aloca recursos de sistema independentes, como espaço de memória, para o processo.
-
Thread : Um thread é uma unidade de execução em um processo, que é responsável por concluir a tarefa de executar o programa atual.Existe pelo menos um thread em um processo. Pode haver vários encadeamentos em um processo, e esse programa aplicativo também pode ser chamado de programa multiencadeado neste momento. O multi-threading permite que os programas sejam executados simultaneamente e façam uso total dos recursos da CPU.
Pergunta da entrevista : Um processo é a menor unidade de agendamento do sistema operacional e alocação de recursos, e um thread é a menor unidade de agendamento da CPU. Diferentes processos não compartilham memória. O custo de troca de dados e comunicação entre processos é alto. Diferentes threads compartilham a memória do mesmo processo. Obviamente, threads diferentes também têm seu próprio espaço de memória independente. Para a área de método, a memória do mesmo objeto no heap pode ser compartilhada entre as threads, mas as variáveis locais da pilha são sempre independentes.
Vantagens e cenários de aplicação do multithreading
- A principal vantagem:
- Faça uso total das fatias de tempo ocioso da CPU para concluir as solicitações do usuário no menor tempo possível. Isso é para fazer o programa responder mais rápido.
- Cenário de aplicação:
- multitarefa. Quando vários usuários solicitam o servidor, o programa do servidor pode abrir vários threads para processar a solicitação de cada usuário separadamente, sem afetar uns aos outros.
- Processamento de tarefa grande única. Para baixar um arquivo grande, você pode abrir vários threads para baixar juntos, reduzindo o tempo geral de download.
agendamento de encadeamento
Refere-se a como os recursos da CPU são alocados para diferentes threads. Dois métodos comuns de escalonamento de threads:
-
agendamento de compartilhamento de tempo
Todos os threads se revezam para usar a CPU e cada thread ocupa o tempo da CPU uniformemente.
-
agendamento preventivo
Dê prioridade a threads com alta prioridade para usar a CPU.Se as threads tiverem a mesma prioridade, uma será selecionada aleatoriamente (aleatoriedade de thread).Java usa um método de escalonamento preemptivo .
2. Criação e inicialização do thread
Herdar a classe Thread
Etapas para criar e iniciar multithreading herdando a classe Thread:
- Defina a subclasse da classe Thread e reescreva o método run() dessa classe. O corpo do método run() representa a tarefa que a thread precisa concluir, então o método run() é chamado de corpo de execução da thread .
- Crie uma instância da subclasse Thread, ou seja, crie um objeto thread
- Chame o método start() do objeto thread para iniciar o thread
Notas sobre a análise de execução multi-threaded :
-
Chamar manualmente o método run não é a maneira de iniciar um thread, é apenas uma chamada de método normal.
-
Após o método start iniciar o thread, o método run será chamado e executado pela JVM.
-
Não inicie o mesmo thread repetidamente, caso contrário, uma exceção será lançada
IllegalThreadStateException
-
Não use a unidade Junit para testar multi-threading, não é suportado, após o término do thread principal, ele chamará para
System.exit()
sair da JVM diretamente;
Implemente a interface Runnable
- Defina a classe de implementação da interface Runnable e reescreva o método run() da interface. O corpo do método run() também é o corpo de execução do thread.
- Crie uma instância da classe de implementação Runnable e use essa instância como o destino de Thread para criar um objeto Thread, que é o objeto thread real.
- Chame o método start() do objeto thread para iniciar o thread.
Comparação de duas formas de criar threads
-
A própria classe Thread também implementa a interface Runnable.O método run vem da interface Runnable, e o método run também é a tarefa de thread real a ser executada.
public class Thread implements Runnable { }
-
Como as classes Java são herdadas de forma única, a maneira de herdar Thread tem a limitação de herança única, mas é mais simples de usar.
-
A maneira de implementar a interface Runnable evita a limitação de herança única e pode criar vários objetos de threadCompartilhar um objeto de classe de implementação Runnable (classe de tarefa de encadeamento), de forma a facilitar a execução de tarefas multi-threadedcompartilhar dados.
Thread de criação de objeto de classe interna anônima
Criar threads por meio de objetos de classe interna anônimos não é uma nova maneira de criar threads, mas quando a tarefa de thread precisa ser executada apenas uma vez, não precisamos criar classes de thread separadamente, podemos usar objetos anônimos:
new Thread("新的线程!"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}.start();
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":" + i);
}
}
}).start();
3. Classe de rosca
método de construção
- public Thread() : Aloca um novo objeto de thread.
- public Thread(String name): Aloca um novo objeto de thread com o nome especificado.
- public Thread(Runnable target): Aloca um novo objeto de thread com o alvo especificado.
- public Thread(Runnable target, String name): Aloca um novo objeto de thread com o alvo especificado e especifica o nome.
Threads usam o método básico
-
public void run() : A tarefa a ser executada por este thread é definida aqui.
-
public String getName() : Obtém o nome do thread atual.
-
public static Thread currentThread() : Retorna uma referência ao objeto thread em execução no momento.
-
public final boolean isAlive(): Testa se a thread está ativa. Ativo se o thread foi iniciado e ainda não foi encerrado.
-
public final int getPriority() : retorna a prioridade do thread
-
public final void setPriority(int newPriority): altera a prioridade do thread
- Cada thread tem uma certa prioridade e threads com prioridade mais alta terão mais oportunidades de execução. Por padrão, cada thread tem a mesma prioridade que o thread pai que o criou. A classe Thread fornece as classes de método setPriority(int newPriority) e getPriority() para definir e obter a prioridade do thread, onde o método setPriority requer um número inteiro e o intervalo está entre [1,10]. defina as três prioridades das constantes da classe Thread:
- MAX_PRIORITY (10): prioridade mais alta
- MIN_PRIORITY (1): prioridade mais baixa
- NORM_PRIORITY (5): prioridade normal, por padrão a thread principal tem prioridade normal.
Métodos comuns de controle de encadeamento
-
public void start() : Faz com que este thread comece a executar; a máquina virtual Java chama o método run deste thread.
-
public static void sleep(long millis): Thread sleep, que faz com que o thread atualmente em execução pare (interrompa temporariamente a execução) pelo número especificado de milissegundos.
-
public static void yield(): Polidez do encadeamento, rendimento apenas faz com que o encadeamento atual perca temporariamente o direito de executar, permite que o agendador de encadeamento do sistema reagende, esperando que outros encadeamentos com a mesma ou maior prioridade que o encadeamento atual possam ter a chance de executar , mas isso não é garantido. É totalmente possível que quando um thread chama o método yield para pausar, o agendador de threads agende-o para reexecução.
-
void join() : Junte-se a um thread, adicione um novo thread ao thread atual, espere o thread unido terminar antes de continuar a executar o thread atual.
void join(long millis): espera até que este thread termine por até millis milissegundos. Se o tempo de milésimos acabar, não haverá mais espera.
void join(long millis, int nanos): Aguarde até que o thread termine por até millis millis + nanos nanossegundos.
-
public final void stop(): Força o thread a parar de executar. Este método é inseguro, obsoleto e não deve ser usado.
- Chamar o método stop() interromperá imediatamente todo o trabalho restante no método run(), incluindo aqueles na instrução catch ou finalmente, e lançará uma exceção ThreadDeath (geralmente essa exceção não requer uma captura explícita), portanto, pode causar alguns O trabalho de limpeza não pode ser concluído, como fechar arquivos, bancos de dados, etc.
- Chamar o método stop() liberará imediatamente todos os bloqueios mantidos pelo thread, resultando em dados não sincronizados e inconsistência de dados.
-
public void interrupt(): Interromper o encadeamento na verdade marca o encadeamento como uma interrupção e não interrompe a execução do encadeamento.
-
public static boolean interrupt(): Verifique o status de interrupção do thread, chamando este método irá limpar o status de interrupção (flag).
-
public boolean isInterrupted(): Verifica o status do thread interrompido, não irá limpar o status interrompido (sinalizador)
-
public void setDaemon(boolean on): define a thread como uma thread daemon. Deve ser definido antes do início do thread, caso contrário,
IllegalThreadStateException
uma exceção será relatada.- A thread daemon serve principalmente a outras threads. Quando não há nenhuma thread não-daemon em execução no programa, a thread daemon também encerrará a execução. O coletor de lixo JVM também é um encadeamento daemon.
-
public boolean isDaemon(): Verifique se a thread atual é uma thread daemon.
-
O papel de volátil é garantir que algumas instruções não sejam omitidas devido à otimização do compilador. A variável volátil significa que a variável pode ser alterada inesperadamente. Releia cuidadosamente o valor dessa variável todas as vezes, em vez de usar salvar um backup no registrar, para que o compilador não assuma o valor dessa variável.
ciclo de vida do fio
Cinco estados de rosca do modelo de rosqueamento tradicional
Os seis estados de encadeamento definidos pelo JDK definem uma classe de enumeração dentro da classe para descrever os seis estados do encadeamento
:java.lang.Thread
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
Descrição: Ao retornar de WAITING
ou para o estado, se for constatado que o thread atual não obteve o bloqueio do monitor, ele será imediatamente transferido para o estado.TIMED_WAITING
Runnable
BLOCKED
segurança do fio
Quando utilizamos várias threads para acessar o mesmo recurso (pode ser a mesma variável, o mesmo arquivo, o mesmo registro etc.), mas se houver operações de leitura e gravação no recurso em várias threads, haverá inconsistência de dados problemas antes e depois, que é um problema de segurança do thread.
Problemas de segurança de rosca levam a
- Variáveis locais não podem ser compartilhadas : variáveis locais são independentes cada vez que o método é chamado, então os dados de run() de cada thread são independentes, não dados compartilhados.
- As variáveis de instância de diferentes objetos não são compartilhadas : as variáveis de instância de diferentes objetos de instância são independentes.
- variáveis estáticas são compartilhadas
- Variáveis de instância do mesmo objeto são compartilhadas
Resumo: problemas de segurança de rosca surgem devido às seguintes condições
- execução multithread
- compartilhar dados
- Várias instruções operam em dados compartilhados
Solução para o problema de segurança da rosca
Método de sincronização : A palavra-chave sincronizada modifica diretamente o método, indicando que apenas um encadeamento pode entrar nesse método ao mesmo tempo e outros encadeamentos estão esperando do lado de fora.
public synchronized void method(){
可能会产生线程安全问题的代码
}
Bloco de código sincronizado : A palavra-chave sincronizado pode ser usada na frente de um determinado bloco, indicando que apenas os recursos desse bloco são mutuamente exclusivos.
synchronized(同步锁){
需要同步操作的代码
}
Bloquear seleção de objeto
preferiu issoseguido pelaclasse.classepode ser também" "
objeto string vazio
Objeto de bloqueio de sincronização:
- Objetos de bloqueio podem ser de qualquer tipo.
- Vários objetos de encadeamento usam o mesmo bloqueio.
O objeto de bloqueio do bloco de código sincronizado
- No bloco de código estático: use o objeto Class da classe atual
- Em blocos de código não estáticos: é comum considerar isso primeiro, mas preste atenção se o mesmo
O escopo do bloqueio é muito pequeno: ele não pode resolver problemas de segurança e todas as instruções que operam em recursos compartilhados devem ser sincronizadas.
O escopo do bloqueio é muito grande: porque uma vez que um thread agarra o bloqueio, outros threads só podem esperar, então o escopo é muito grande, a eficiência será reduzida e os recursos da CPU não poderão ser usados razoavelmente.
Problemas de segurança de encadeamento do padrão de design singleton
1. O estilo chinês faminto não tem problemas de segurança de rosca
Estilo chinês faminto: crie objetos assim que aparecer
2. Problemas de segurança de encadeamento de estilo preguiçoso
public class Singleton {
private static Singleton ourInstance;
public static Singleton getInstance() {
//一旦创建了对象,之后再次获取对象,都不会再进入同步代码块,提升效率
if (ourInstance == null) {
//同步锁,锁住判断语句与创建对象并赋值的语句
synchronized (Singleton.class) {
if (ourInstance == null) {
ourInstance = new Singleton();
}
}
}
return ourInstance;
}
private Singleton() {
}
}
Aguardando o mecanismo de despertar
Quando um thread atende a uma determinada condição, ele entra no estado de espera ( wait() / wait(time) ), espera que outros threads executem seu código especificado e, em seguida, o ativa ( notify() ); ou você pode especificar a espera time , espere o tempo acordar automaticamente; quando houver vários threads esperando, se necessário, você pode usar notifyAll() para acordar todos os threads em espera. wait/notify é um mecanismo de coordenação entre threads.
- wait: A thread não está mais ativa, não participa mais do escalonamento e entra no conjunto de espera, portanto não desperdiçará recursos da CPU e não competirá por bloqueios. O estado da thread neste momento é WAITING ou TIMED_WAITING. Ele também espera que outras threads executem uma ação especial , ou seja, " notificar " ou quando o tempo de espera acabar, a thread que está esperando por este objeto é liberada do wait set e entra novamente na fila de dispatch (ready queue) ) meio
- notificar: selecione um thread no conjunto de espera do objeto notificado para liberar;
- notifyAll: Libera todos os encadeamentos no conjunto de espera do objeto notificado.
Obs:
Após a thread notificada ser acordada, ela pode não conseguir retomar a execução imediatamente, pois o local onde foi interrompida era no bloco de sincronização, e neste momento ela não está mais segurando o lock, então ela precisa tentar adquirir o bloqueio novamente (provavelmente enfrentando outra concorrência do Thread), somente após o sucesso a execução pode ser retomada no local após o qual o método wait foi originalmente chamado.
Resumido da seguinte forma:
- Se o bloqueio puder ser adquirido, o thread muda do estado WAITING para o estado RUNNABLE (executável);
- Caso contrário, a thread muda do estado WAITING para o estado BLOCKED (aguardando bloqueio).
Os detalhes que precisam ser observados ao chamar os métodos wait e notify
- O método wait e o método notify devem ser chamados pelo mesmo objeto de bloqueio. Porque: o objeto de bloqueio correspondente pode ativar o thread após o método wait chamado com o mesmo objeto de bloqueio por meio de notificação.
- O método wait e o método notify pertencem aos métodos da classe Object. Porque: o objeto de bloqueio pode ser qualquer objeto e a classe de qualquer objeto herda a classe Object.
- O método wait e o método notify devem ser usados em um bloco de código de sincronização ou em uma função de sincronização, e esses dois métodos devem ser chamados por meio do objeto de bloqueio.
Operação de bloqueio de liberação e bloqueio
1. A operação de liberação da trava
-
A execução do método de sincronização e do bloco de código de sincronização do thread atual termina.
-
Ocorre um erro ou exceção não tratada no bloco de código de sincronização ou no método de sincronização do thread atual, fazendo com que o thread atual termine de forma anormal.
-
O thread atual executa o método wait() do objeto de bloqueio no bloco de código de sincronização e no método de sincronização, o thread atual é suspenso e o bloqueio é liberado.
2. Impasse
Diferentes threads bloqueiam o objeto do monitor de sincronização exigido pela outra parte e não o liberam.Quando eles estão esperando que a outra parte desista primeiro, um impasse de thread é formado. Quando ocorre um impasse, todo o programa não será anormal nem fornecerá nenhum prompt, mas todos os encadeamentos serão bloqueados e não poderão continuar.
3. Pergunta da entrevista: a diferença entre os métodos sleep() e wait()
(1) sleep() não libera o bloqueio, wait() libera o bloqueio
(2) sleep() especifica o tempo de inatividade, wait() pode especificar o tempo ou esperar indefinidamente até notificar ou notificarTodos
(3) sleep() é um método estático declarado na classe Thread, e o método wait é declarado na classe Object
Porque chamamos o método wait () é chamado pelo objeto de bloqueio e o tipo do objeto de bloqueio é qualquer tipo de objeto. Então os métodos que você deseja que qualquer tipo de objeto tenha só podem ser declarados na classe Object.
prática
Duas threads são necessárias para imprimir letras ao mesmo tempo, e cada thread pode imprimir 3 letras continuamente. Dois threads imprimem alternadamente, um thread imprime a forma minúscula da letra e um thread imprime a forma maiúscula da letra, mas as letras são consecutivas. Depois que a letra fizer um loop para z, volte para a.
public class PrintLetterDemo {
public static void main(String[] args) {
// 2、创建资源对象
PrintLetter p = new PrintLetter();
// 3、创建两个线程打印
new Thread("小写字母") {
public void run() {
while (true) {
p.printLower();
try {
Thread.sleep(1000);// 控制节奏
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread("大写字母") {
public void run() {
while (true) {
p.printUpper();
try {
Thread.sleep(1000);// 控制节奏
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
// 1、定义资源类
class PrintLetter {
private char letter = 'a';
public synchronized void printLower() {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName() + "->" + letter);
letter++;
if (letter > 'z') {
letter = 'a';
}
}
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void printUpper() {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName() + "->" + (char) (letter - 32));
letter++;
if (letter > 'z') {
letter = 'a';
}
}
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}