Executor de pool de threads

Desvantagens do novo Tópico

  • Cada vez que um novo Thread cria um novo objeto, o desempenho é ruim
  • Threads carecem de gerenciamento unificado e pode haver novos threads ilimitados competindo entre si e podem ocupar muitos recursos do sistema e causar travamentos ou OOM (sem memória). (A razão para este problema não é simplesmente um novo Thread, mas pode ser causado por um novo Thread contínuo devido a bugs do programa ou falhas de design)
  • Falta de mais funções, como mais execução, execução regular, interrupção do thread.

Benefícios do pool de threads

  1. Reduza o consumo de recursos . Reutilize o encadeamento criado para reduzir a sobrecarga de criação e morte do encadeamento.
  2. Melhore a velocidade de resposta . Quando uma tarefa chega, ela pode ser executada imediatamente sem esperar que o thread seja criado.
  3. Melhore a capacidade de gerenciamento de threads . Threads são recursos escassos. Se forem criados ilimitadamente, não apenas consumirão recursos do sistema, mas também reduzirão a estabilidade do sistema. O pool de threads pode ser usado para alocação, ajuste e monitoramento uniformes. (Controle simultâneo, tempo / execução regular, etc.)

Diagrama de classe de pool de threads

Diagrama de classe de pool de threads
Os executores na parte inferior são comumente usados ​​e os usam para criar pools de threads e usar threads.
O framework Executor programa a execução e o controle de tarefas assíncronas de acordo com um conjunto de estratégias de execução, com o objetivo de fornecer um mecanismo para separar o envio de tarefas da operação de tarefas .

  • Executor: uma interface simples para executar novas tarefas
  • ExecutorService: estende o Executor e adiciona métodos para gerenciar o ciclo de vida do executor e o ciclo de vida das tarefas
  • ScheduleExcutorService: Extended ExecutorService para suportar tarefas futuras e agendadas

O princípio de realização do pool de threads

Fluxo de processamento

A relação entre corePoolSize, maximumPoolSize e workQueue

  • corePoolSize (número de threads principais, número de threads básicas): Ao enviar uma tarefa para o pool de threads, o pool de threads criará uma thread para realizar a tarefa (mesmo se houver threads básicas inativos. Quando o número de tarefas for maior que corePoolSize, ele não será criado). Se o método prestartAllCoreThreads () do conjunto de encadeamentos for chamado, o conjunto de encadeamentos criará e iniciará todos os encadeamentos básicos com antecedência.
  • runnableTaskQueue (fila de tarefas): fila de bloqueio workQueue, armazenamento de tarefas esperando para serem executadas (quando o número de threads em execução é maior que corePoolSize e menor que maximumPoolSize)
  • maximumPoolSize: O número máximo de threads. Se a fila estiver cheia e o número de encadeamentos criados for menor que o número máximo de encadeamentos, o pool de encadeamentos criará novos encadeamentos para executar tarefas. (Se uma fila de tarefas ilimitada for usada, o número máximo de threads que podem ser criados é corePoolSize, então maximumPoolSize não funcionará)

Taxa de transferência: SynchronousQueue é maior que LinkedBlockingQueue e ArrayBlockingQueue
Diagrama esquemático da execução de ThreadPoolExecutor

4 situações em que ThreadPoolExecutor executa execute ()

1) Se o encadeamento atualmente em execução for menor que corePoolSize, crie um novo encadeamento para realizar a tarefa (você precisa adquirir um bloqueio global ).
2) Se o thread em execução for igual ou maior que corePoolSize, a tarefa será adicionada ao BlockingQueue.
3) Se a tarefa não puder ser adicionada ao BlockingQueue (a fila está cheia), um novo thread é criado para processar a tarefa (um bloqueio global precisa ser adquirido ).
4) Se a criação de um novo encadeamento fizer com que o encadeamento atualmente em execução exceda o máximoPoolSize, a tarefa será rejeitada e o método RejectedExecutionHandler.rejectedExecution () será chamado.

设计思路, É para evitar a aquisição de um bloqueio global (um sério gargalo de escalabilidade ) tanto quanto possível ao executar o método execute () . Depois que ThreadPoolExecutor termina de aquecer (o número de threads em execução é maior ou igual a corePoolSize), quase todas as chamadas de método execute () são executadas na etapa 2, e a etapa 2 não precisa adquirir um bloqueio global.

jdk版本不同会有区别
public void execute(Runnable command) {
    
    
        if (command == null)
            throw new NullPointerException();
         //只有当前线程池中线程数poolSize <corePoolSize 时,则创建线程并执行当前任务 
        //当poolSize >=corePoolSize 或线程创建失败,则将当前任务放到工作队列中。  
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
    
       
            if (runState == RUNNING && workQueue.offer(command)) {
    
    
                //如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许的线程数量,则创建一个线程执行任务。
                if (runState != RUNNING || poolSize == 0)
                    ensureQueuedTaskHandled(command);
            }
           //抛出RejectedExecutionException异常
            else if (!addIfUnderMaximumPoolSize(command))
                reject(command); // is shutdown or saturated
        }
    }

Thread de trabalho: quando o pool de threads cria um thread, ele encapsula o thread em um Worker de thread de trabalho. Depois que o trabalhador termina de executar a tarefa, ele também obtém as tarefas na fila de trabalho para execução em um loop ( não será GC ) O método run () da classe Worker:

 public void run() {
	    try {
	    	Runnable task = firstTask;
	    	firstTask = null;
	    	while (task != null || (task = getTask()) != null) {
		    	runTask(task);
		    	task = null;
	    	}
	    } finally {
	    	workerDone(this);
    	}
    }

Criação de pool de threads

Podemos criar um pool de threads por meio de ThreadPoolExecutor.

new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long  keepAliveTime,TimeUnit unit  ,
BlockingQueue<Runnable> workQueue ,ThreadFactory threadFactory, RejectedExecutionHandler handler);

ThreadFactory: Thread factory, definido para criar threads, você pode definir um nome para cada thread criado através da thread factory. O ThreadFactoryBuilder fornecido pela estrutura de software livre guava pode definir rapidamente nomes significativos para threads no pool de threads. O código é o seguinte.

new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();

RejectedExecutionHandler (estratégia de saturação): Quando a fila e o pool de threads estão cheios, indicando que o pool de threads está em um estado saturado, uma estratégia deve ser adotada para lidar com as novas tarefas enviadas.

.AbortPolicy:直接抛出异常。
·CallerRunsPolicy:使用 【调用者 dubbo生产者主线程】所在线程(execute 方法的调用线程) 来运行任务(可能会影响主线程)。
·DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
·DiscardPolicy:不处理,丢弃掉。

Você também pode implementar uma estratégia personalizada para a interface RejectedExecutionHandler de acordo com as necessidades do cenário do aplicativo. Como registro ou armazenamento persistente não pode lidar com tarefas.

keepAliveTime (tempo de retenção da atividade do thread): O tempo que os threads de trabalho do pool de threads permanecem ativos após ficarem ociosos. Portanto, se houver muitas tarefas e o tempo de execução de cada tarefa for relativamente curto, o tempo pode ser ajustado para aumentar a utilização de threads.

TimeUnit: a unidade de tempo de keepAliveTime

Explicação detalhada de três métodos estáticos de executores de classe de fábrica

Envie tarefas para o pool de threads

O método execute () é usado para enviar tarefas que não requerem um valor de retorno , portanto, é impossível determinar se a tarefa foi executada com sucesso pelo pool de threads.
O código a seguir mostra que a tarefa inserida pelo método execute () é uma instância da classe Runnable.

threadsPool.execute(new Runnable() {
	@Override
	public void run() {
		// TODO Auto-generated method stub
	}
});

O método submit () é usado para enviar tarefas que requerem um valor de retorno . O pool de threads retornará um objeto de tipo futuro . Por meio desse objeto futuro, pode-se avaliar se a tarefa foi executada com sucesso e o valor de retorno pode ser obtido por meio do método get () do futuro. O método get () bloqueará o encadeamento atual até que a tarefa seja concluída e use get O método (tempo limite longo, unidade TimeUnit) bloqueará o encadeamento atual por um período de tempo e retornará imediatamente. Nesse momento, a tarefa pode não ser concluída.

Future<Object> future = executor.submit(harReturnValuetask);
	try {
		Object s = future.get();//获取返回值
	} catch (InterruptedException e) {
		// 处理中断异常
	} catch (ExecutionException e) {
		// 处理无法执行任务异常
	} finally {
		// 关闭线程池
		executor.shutdown();
	}

Fechar pool de discussão

Ciclo de vida do pool de threads
O método shutdown ou shutdownNow fecha o pool de threads.
Princípio : Percorra os threads de trabalho no pool de threads e, a seguir, chame o método de interrupção do thread, um por um, para interromper o thread, de forma que a tarefa que não pode responder à interrupção nunca seja encerrada.

a diferença

  • shutdownNow () define o estado do pool de threads como STOP, tenta parar todas as threads que estão executando ou suspendendo tarefas e retorna a lista de tarefas esperando para serem executadas
  • shutdown () define o estado do pool de threads para o estado SHUTDOWN e interrompe todos os threads que não estão executando tarefas.

Outro status

  • SHUTDOWN não pode processar novas tarefas, mas pode continuar a processar tarefas na fila de bloqueio
  • parar: não pode receber novas tarefas, nem processar tarefas na fila
  • arrumação: Se todas as tarefas foram encerradas, o número de threads efetivos é 0
  • terminado: estado final
  /**
     * Initiates an orderly shutdown in which previously submitted
     * tasks are executed, but no new tasks will be accepted.
     * Invocation has no additional effect if already shut down.
     */
    public void shutdown() {
    
    
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
    
    
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
    
    
            mainLock.unlock();
        }
        tryTerminate();
    }

  /**
     * Attempts to stop all actively executing tasks, halts the
     * processing of waiting tasks, and returns a list of the tasks
     * that were awaiting execution. These tasks are drained (removed)
     * from the task queue upon return from this method.
     */
    public List<Runnable> shutdownNow() {
    
    
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
    
    
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
    
    
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

Configure razoavelmente o pool de threads

As características da tarefa devem ser analisadas primeiro, o que pode ser analisado a partir das seguintes perspectivas.

  • A natureza da tarefa: tarefas intensivas de CPU, tarefas intensivas de E / S e tarefas mistas.
  • A prioridade da tarefa: alta, média e baixa.
  • Tempo de execução da tarefa: longo, médio e curto.
  • Dependência de tarefa: se depende de outros recursos do sistema, como conexões de banco de dados.

Tarefas de natureza diferente podem ser processadas separadamente por pools de threads de tamanhos diferentes.

  • Tarefas com uso intensivo de CPU devem ser configuradas com o mínimo de threads possível, como configurar um pool de threads de Ncpu + 1 threads.
  • Como os threads de tarefa com IO intensivo nem sempre estão executando tarefas, você deve configurar o máximo de threads possível, como 2 * Ncpu.
  • Se uma tarefa mista puder ser dividida, divida-a em uma tarefa com uso intensivo de CPU e uma tarefa com uso intensivo de E / S. Contanto que a diferença de tempo entre a execução das duas tarefas não seja muito grande, a taxa de transferência após a decomposição será maior do que Taxa de transferência de execução serial. Se o tempo de execução dessas duas tarefas for muito diferente, não há necessidade de decompor.
  • Confie na tarefa do pool de conexão do banco de dados, porque o thread precisa esperar que o banco de dados retorne o resultado após o envio do SQL. Quanto maior o tempo de espera, maior o tempo ocioso da CPU. Então, o número de threads deve ser definido maior , para aproveitar melhor a CPU.

O número de CPUs do dispositivo atual pode ser obtido através do método Runtime.getRuntime (). AvailableProcessors ().
Tarefas com prioridades diferentes podem ser processadas usando a fila de prioridade PriorityBlockingQueue.
Nota : Se sempre houver tarefas de alta prioridade enviadas para a fila, as tarefas de baixa prioridade podem nunca ser executadas.

É recomendável usar o
máximo maximumPoolSize da fila limitada , o que pode aumentar a estabilidade e a capacidade de aviso antecipado do sistema e pode reduzir o consumo de recursos.No entanto, esse método torna o agendamento de threads mais difícil para o pool de threads.
Eu quero fazer [taxa de transferência do pool de threads] [tarefas de processamento] atingir um intervalo razoável, de modo que [o agendamento da thread é relativamente simples] [reduza o consumo de recursos o máximo possível] limite razoavelmente [thread pool] e [capacidade da fila]

Habilidades de atribuição

  1. Reduza o consumo de recursos [uso de CPU, consumo de recursos do sistema operacional, sobrecarga de troca de contexto] defina uma [capacidade de fila maior] [capacidade de pool de encadeamento menor] para reduzir a taxa de transferência do pool de encadeamento
  2. As tarefas enviadas frequentemente bloqueiam, você pode ajustar o maximumPoolSize
  3. A capacidade da fila é pequena e o tamanho do pool de threads precisa ser definido maior, para que a taxa de uso da CPU seja relativamente maior
  4. Se a capacidade do pool de encadeamentos for definida muito grande e o número de tarefas for muito aumentado, a quantidade de simultaneidade aumentará e o agendamento entre os encadeamentos precisará ser considerado. Isso pode reduzir o rendimento das tarefas de processamento.

Monitoramento de pool de threads

· TaskCount: O número de tarefas que o pool de threads precisa executar.
· CompletedTaskCount: O número de tarefas concluídas pelo pool de threads durante o processo de execução, que é menor ou igual a taskCount.
· LargestPoolSize: O número máximo de threads já criado no pool de threads. Por meio desses dados, você pode saber se o pool de threads está cheio. Se o valor for igual ao tamanho máximo do conjunto de encadeamentos, isso significa que o conjunto de encadeamentos está cheio.
· GetPoolSize: o número de threads no pool de threads. Se o thread pool não for destruído, os threads no pool não serão destruídos automaticamente, portanto, o tamanho só aumentará.
· GetActiveCount: Obtenha o número de threads ativos.

Monitore expandindo o pool de threads. Você pode personalizar o pool de threads herdando o pool de threads, reescrever os métodos beforeExecute, afterExecute e encerrados do pool de threads ou executar algum código para monitorar antes, depois e antes da tarefa ser executada e antes que o pool de threads seja fechado . Por exemplo, monitore o tempo médio de execução, o tempo máximo de execução e o tempo mínimo de execução das tarefas.

protected void beforeExecute(Thread t, Runnable r) {
	这几个方法在线程池里是空方法。
 }

Acho que você gosta

Origin blog.csdn.net/eluanshi12/article/details/85232483
Recomendado
Clasificación