Pool de threads ThreadPoolExecutor princípio subjacente à análise do código-fonte

Qual é o processo específico de tarefas de execução do pool de threads?

ThreadPoolExecutor fornece dois métodos para executar tarefas:

  • void execute (comando executável)
  • Future<?> submit(tarefa executável)

Na verdade, o método execute() é finalmente chamado no submit, mas retorna um objeto Future para obter o resultado da execução da tarefa:

public Future<?> submit(Runnable task) {
    
    
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

O método execute (comando Runnable) será executado em três etapas:
Insira a descrição da imagem aqui

Perceber:

  • Ao enviar um Runnable, independentemente de os threads no pool de threads atual estarem ociosos ou não, novos threads serão criados desde que o número seja menor que o número de threads principais.
  • ThreadPoolExecutor é equivalente a injustiça. Por exemplo, um Runnable enviado após a fila estar cheia pode ser executado antes do Runnable ser enfileirado.

Como fluem os cinco estados do pool de threads?

O pool de threads tem cinco estados

  • EM EXECUÇÃO: Novas tarefas serão recebidas e as tarefas na fila serão processadas
  • SHUTDOWN: Novas tarefas não serão aceitas e as tarefas na fila serão processadas
  • PARAR: Novas tarefas não serão recebidas e as tarefas na fila não serão processadas, e as tarefas em processamento serão interrompidas (nota: se uma tarefa pode ser interrompida depende da própria tarefa)
  • TIDYING: Todas as tarefas foram encerradas e não há threads no pool de threads, então o status do pool de threads mudará para TIDYING. Assim que esse status for atingido, terminado() do pool de threads será chamado.
  • TERMINATED: Após a execução determinated(), ele mudará para TERMINATED

Estes cinco estados não podem ser convertidos arbitrariamente, havendo apenas as seguintes situações de conversão:

  • RUNNING -> SHUTDOWN: Acionado pela chamada manual de shutdown(), ou finalize() será chamado durante o GC do objeto do pool de threads para chamar shutdown()
  • (RUNNING ou SHUTDOWN) -> STOP: Acionado pela chamada de shutdownNow(). Se shutdown() for chamado primeiro e shutdownNow() for chamado imediatamente, SHUTDOWN -> STOP ocorrerá.
  • SHUTDOWN -> TIDYING: conversão automática quando a fila está vazia e não há threads no pool de threads
  • STOP -> TIDYING: Alterna automaticamente quando não há threads no pool de threads (pode haver tarefas na fila)
  • TIDYING -> TERMINATED: Será convertido automaticamente após a execução determinated().

Como os threads no pool de threads são fechados

  • Normalmente usamos o método thread.start() para iniciar um thread, mas como interrompê-lo?
  • A classe Thread fornece um stop(), mas está marcado como @Deprecated. Por que não é recomendado usar o método stop() para interromper o thread?
  • Como o método stop() é muito grosseiro, uma vez chamado stop(), o thread será interrompido diretamente. No entanto, ao chamar, você não tem ideia do que o thread estava fazendo ou em que etapa a tarefa atingiu. Isso é muito perigoso.
  • É enfatizado aqui que stop() liberará o bloqueio sincronizado ocupado pelo thread (o bloqueio ReentrantLock não será liberado automaticamente, o que também é um fator pelo qual stop() não é recomendado) .
public class ThreadTest {
    
    
    static int count = 0;
    static final Object lock = new Object();
    static final ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
    
    

        Thread thread = new Thread(new Runnable() {
    
    
            public void run() {
    
    
//                synchronized (lock) {
    
    
                reentrantLock.lock();
                for (int i = 0; i < 100; i++) {
    
    
                    count++;
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                }
//                }
                reentrantLock.unlock();
            }
        });

        thread.start();
        Thread.sleep(5*1000);
        thread.stop();
//
//        Thread.sleep(5*1000);

        reentrantLock.lock();
        System.out.println(count);
        reentrantLock.unlock();

//        synchronized (lock) {
    
    
//            System.out.println(count);
//        }
    }
}

Portanto, recomendamos interromper um thread personalizando uma variável ou interrompendo, como:

public class ThreadTest {
    
    

    static int count = 0;
    static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
    
    

        Thread thread = new Thread(new Runnable() {
    
    
            public void run() {
    
    

                for (int i = 0; i < 100; i++) {
    
    
                    if (stop) {
    
    
                        break;
                    }

                    count++;
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                }
            }
        });

        thread.start();
        Thread.sleep(5 * 1000);
        stop = true;
        Thread.sleep(5 * 1000);
        System.out.println(count);
    }
}

A diferença é que quando definimos stop como verdadeiro, o próprio thread pode controlar se e quando parar. Da mesma forma, podemos chamar interrupção() do thread para interromper o thread:

public class ThreadTest {
    
    

    static int count = 0;
    static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
    
    

        Thread thread = new Thread(new Runnable() {
    
    
            public void run() {
    
    

                for (int i = 0; i < 100; i++) {
    
    
                    if (Thread.currentThread().isInterrupted()) {
    
    
                        break;
                    }

                    count++;
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        break;
                    }
                }
            }
        });

        thread.start();
        Thread.sleep(5 * 1000);
        thread.interrupt();
        Thread.sleep(5 * 1000);
        System.out.println(count);
    }
}

A diferença é que se o thread for interrompido durante o sono, uma exceção será recebida.
Na verdade, interrupção() é usada para interromper threads no pool de threads . Por exemplo, o método shutdownNow() chamará

void interruptIfStarted() {
    
    
    Thread t;
    if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
    
    
        try {
    
    
            t.interrupt();
        } catch (SecurityException ignore) {
    
    
        }
    }
}

Por que o pool de threads precisa ser uma fila de bloqueio?

  Durante o processo de execução, os threads no pool de threads continuarão a obter tarefas da fila e a executá-las após a execução da primeira tarefa vinculada ao criar o thread.Então, se não houver tarefas na fila, o thread não morrerá naturalmente. Ele será bloqueado ao adquirir a tarefa da fila, e quando houver uma tarefa na fila, ele irá pegar a tarefa e executar a tarefa.
  Este método pode, em última análise, garantir que um número especificado de threads principais possa ser reservado no pool de threads. O código-chave é:

try {
    
    
    Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
        workQueue.take();
    if (r != null)
        return r;
    timedOut = true;
} catch (InterruptedException retry) {
    
    
    timedOut = false;
}

  Quando um thread obtém uma tarefa da fila, ele determinará se deve usar o bloqueio de tempo limite para obtê-la.Podemos pensar que o thread não principal fará poll(), o thread principal levará() e o thread não principal naturalmente não conseguirá obter a tarefa após o tempo expirar.

Se ocorrer uma exceção em um thread, ele será removido do pool de threads?

A resposta é sim, então é possível que o número de threads principais esteja errado ao executar tarefas, fazendo com que todos os threads principais sejam removidos do pool de threads?
Insira a descrição da imagem aqui

  • No código-fonte, quando ocorre uma exceção durante a execução de uma tarefa, processWorkerExit() será eventualmente executado. Após a execução deste método, o thread atual morrerá naturalmente.
  • No entanto, um thread adicional será adicionado ao método processWorkerExit(), para que o número fixo de threads principais possa ser mantido.

Como o Tomcat personaliza o pool de threads

O conjunto de threads usado no Tomcat é org.apache.tomcat.util.threads.ThreadPoolExecutor. Observe que o nome da classe é o mesmo do JUC, mas o nome do pacote é diferente.
O Tomcat criará este pool de threads:

public void createExecutor() {
    
    
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);
}

A fila de entrada é TaskQueue e sua lógica de enfileiramento é:

public boolean offer(Runnable o) {
    
    
    //we can't do any checks
    if (parent==null) {
    
    
        return super.offer(o);
    }

    //we are maxed out on threads, simply queue the object
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) {
    
    
        return super.offer(o);
    }

    //we have idle threads, just add it to the queue
    if (parent.getSubmittedCount()<=(parent.getPoolSize())) {
    
    
        return super.offer(o);
    }

    //if we have less threads than maximum force creation of a new thread
    if (parent.getPoolSize()<parent.getMaximumPoolSize()) {
    
    
        return false;
    }

    //if we reached here, we need to add it to the queue
    return super.offer(o);
}

Especial em:

  • Ao ingressar na fila, entre na fila apenas se o número de threads no pool de threads for igual ao número máximo de pools de threads
  • Ao ingressar na fila, se o número de threads no pool de threads for menor que o número máximo de pools de threads, false será retornado, indicando que a adesão à fila falhou.

Isso controla o pool de threads do Tomcat ao enviar uma tarefa:

  • Ele ainda determinará primeiro se o número de threads é menor que o número de threads principais. Se for menor que o número de threads principais, crie um thread.
  • Se for igual ao número de threads principais, ele ingressará na fila, mas se o número de threads for menor que o número máximo de threads, a adesão à fila falhará e os threads serão criados.

Portanto, à medida que a tarefa é enviada, os threads serão criados primeiro e não entrarão na fila até que o número de threads seja igual ao número máximo de threads.

Claro, há uma lógica relativamente detalhada: ao enviar uma tarefa, se o número de tarefas processadas for menor que o número de threads no pool de threads, ela será adicionada diretamente à fila sem criar um thread, que é o getSubmittedCount no código-fonte acima.

Como definir o número de threads principais e o número máximo de threads no pool de threads

Existem dois parâmetros muito importantes no pool de threads:

  • corePoolSize: o número de threads principais, indicando o número de threads residentes no pool de threads
  • MaximumPoolSize: o número máximo de threads, indicando o número máximo de threads que podem ser abertos no pool de threads

Então, como definir esses dois parâmetros?

As tarefas que somos responsáveis ​​por executar pelo pool de threads são divididas em três situações:

  • Tarefas com uso intensivo de CPU, como encontrar números primos de 1 a 1.000.000
  • Tarefas com uso intensivo de IO, como IO de arquivo e IO de rede
  • tarefas mistas

  As características das tarefas com uso intensivo de CPU são que os threads sempre utilizarão a CPU ao executar tarefas, portanto, neste caso, a troca de contexto do thread deve ser evitada tanto quanto possível.
  Por exemplo, meu computador agora tem apenas uma CPU. Se dois threads estiverem executando a tarefa de encontrar números primos ao mesmo tempo, a CPU precisará realizar alternância adicional de contexto de thread para obter o efeito de paralelismo de thread. Neste momento, o dois threads estão em execução O tempo total é: tempo
  de execução da tarefa 2 + tempo de troca de contexto do thread
  . Se houver apenas um thread e esse thread executar duas tarefas, então o tempo será:
  tempo de execução da tarefa
2.
  Portanto, para tarefas com uso intensivo de CPU, o número de threads é melhor igual ao número do núcleo da CPU, você pode obter o número do núcleo do seu computador por meio da seguinte API:

Runtime.getRuntime().availableProcessors()

No entanto, para responder às solicitações de bloqueio de thread causadas por falhas de página ou outras exceções durante a execução do thread, podemos configurar um thread adicional, de modo que quando um thread temporariamente não precisar da CPU, possa haver um thread substituto para continuar a utilizar a CPU.

Portanto, para tarefas com uso intensivo de CPU, podemos definir o número de threads para: número de núcleos de CPU + 1

Vejamos as tarefas do tipo IO. Quando os threads executam tarefas do tipo IO, eles podem ser bloqueados no IO na maioria das vezes. Se houver 10 CPUs agora, se configurarmos apenas 10 threads para executar tarefas do tipo IO, então será muito difícil. Talvez esses 10 threads estejam bloqueados em IO, então essas 10 CPUs não têm trabalho a fazer. Portanto, para tarefas de IO, geralmente definimos o número de threads para: 2*número de núcleos de CPU.

No entanto, mesmo que esteja definido para 2*CPU core number, pode não ser o ideal. Por exemplo, se houver 10 CPUs e o número de threads for 20, então é possível que esses 20 threads estejam bloqueados no IO no ao mesmo tempo, para que você possa adicionar mais Threads, reduzindo assim a utilização da CPU.

Normalmente, se o tempo de execução de tarefas do tipo IO for maior, pode haver mais threads bloqueados no IO ao mesmo tempo e podemos configurar mais threads. No entanto, mais threads definitivamente não são melhores. Podemos usar o seguinte
fórmula Para calcular:
Número de threads = Número de núcleos de CPU * (1 + tempo de espera do thread / tempo total de execução do thread)

  • Tempo de espera do thread: refere-se ao tempo em que o thread não está usando a CPU, como bloqueio em IO
  • Tempo total de execução do thread: refere-se ao tempo total que um thread leva para concluir uma tarefa

Análise do código-fonte de propriedades e métodos básicos no pool de threads

No código-fonte do conjunto de encadeamentos, uma variável ctl do tipo AtomicInteger é usada para representar o status do conjunto de encadeamentos e o número de encadeamentos em funcionamento no conjunto de encadeamentos atual.
Um número inteiro ocupa 4 bytes, ou seja, 32 bits. O pool de threads possui 5 estados:

  • RUNNING: O pool de threads está funcionando normalmente e pode aceitar e processar tarefas normalmente.
  • DESLIGAMENTO: O pool de threads está fechado e não pode aceitar novas tarefas. No entanto, o pool de threads executará as tarefas restantes na fila de bloqueio. Após o processamento das tarefas restantes, todos os threads de trabalho serão interrompidos.
  • STOP: O pool de threads foi interrompido, não pode aceitar novas tarefas e não processará tarefas na fila de bloqueio, interrompendo todos os threads de trabalho.
  • TIDYING: Depois que todos os threads de trabalho no pool de threads atual forem interrompidos, TIDYING será inserido.
  • TERMINATED: Depois que o pool de threads estiver no estado TIDYING, ele executará o método terminada(). Após a execução, ele entrará no estado TERMINATED. Em ThreadPoolExecutor, terminada() é um método vazio. Você pode personalizar o pool de threads para substituir este método.

2 bits podem representar 4 estados, e esses 5 estados requerem pelo menos três bits. Por exemplo, é assim que é representado no código-fonte do pool de threads:

private static final int COUNT_BITS = Integer.SIZE - 3;

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

Integer.SIZE é 32, então COUNT_BITS é 29. O sistema secundário final correspondente a cada estado é:

  • EM EXECUÇÃO: 11100000 00000000 00000000 00000000
  • DESLIGAMENTO:00000000 00000000 00000000 00000000
  • PARAR:00100000 00000000 00000000 00000000
  • ARRUMAR:01000000 00000000 00000000 00000000
  • TERMINADO:01100000 00000000 00000000 00000000

Portanto, você só precisa usar os três bits mais altos de um número inteiro para representar o status de 5 pools de threads, e os 29 bits restantes podem ser usados ​​para representar o número de threads em funcionamento . Por exemplo, se o ctl for: 11100000 00000000 00000000 00001010 , significa que o status do pool de threads é RUNNING e há 10 threads atualmente trabalhando no pool de threads. O "funcionamento" aqui significa que o thread está ativo, executando tarefas ou bloqueando e aguardando tarefas.

Ao mesmo tempo, o pool de threads também fornece alguns métodos para obter o status do pool de threads e o número de threads em funcionamento, como:

// 29,二进制为00000000 00000000 00000000 00011101
private static final int COUNT_BITS = Integer.SIZE - 3;

// 00011111 11111111 11111111 11111111
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// ~CAPACITY为11100000 00000000 00000000 00000000
// &操作之后,得到就是c的高3位
private static int runStateOf(int c)     {
    
     
    return c & ~CAPACITY; 
}

// CAPACITY为00011111 11111111 11111111 11111111
// &操作之后,得到的就是c的低29位
private static int workerCountOf(int c)  {
    
     
    return c & CAPACITY; 
}

Ao mesmo tempo, existe outro método:

private static int ctlOf(int rs, int wc) {
    
     
    return rs | wc; 
}

É um método usado para combinar o status de execução e o número de threads de trabalho. No entanto, os dois números int passados ​​para este método são limitados. Os 29 bits inferiores de rs devem ser 0 e os 3 bits superiores de wc devem ser 0 , portanto, após a operação OR, o ctl preciso pode ser obtido.

Ao mesmo tempo, existem alguns métodos relacionados

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// c状态是否小于s状态,比如RUNNING小于SHUTDOWN
private static boolean runStateLessThan(int c, int s) {
    
    
    return c < s;
}

// c状态是否大于等于s状态,比如STOP大于SHUTDOWN
private static boolean runStateAtLeast(int c, int s) {
    
    
    return c >= s;
}

// c状态是不是RUNNING,只有RUNNING是小于SHUTDOWN的
private static boolean isRunning(int c) {
    
    
    return c < SHUTDOWN;
}

// 通过cas来增加工作线程数量,直接对ctl进行加1
// 这个方法没考虑是否超过最大工作线程数的(2的29次方)限制,源码中在调用该方法之前会进行判断的
private boolean compareAndIncrementWorkerCount(int expect) {
    
    
    return ctl.compareAndSet(expect, expect + 1);
}

// 通过cas来减少工作线程数量,直接对ctl进行减1
private boolean compareAndDecrementWorkerCount(int expect) {
    
    
    return ctl.compareAndSet(expect, expect - 1);
}

método de execução

Ao executar o método execute do pool de threads:

public void execute(Runnable command) {
    
    
    
    if (command == null)
        throw new NullPointerException();
    
    // 获取ctl
    // ctl初始值是ctlOf(RUNNING, 0),表示线程池处于运行中,工作线程数为0
    int c = ctl.get();
    
    // 工作线程数小于corePoolSize,则添加工作线程,并把command作为该线程要执行的任务
    if (workerCountOf(c) < corePoolSize) {
    
    
        // true表示添加的是核心工作线程,具体一点就是,在addWorker内部会判断当前工作线程数是不是超过了corePoolSize
        // 如果超过了则会添加失败,addWorker返回false,表示不能直接开启新的线程来执行任务,而是应该先入队
        if (addWorker(command, true))
            return;
        
        // 如果添加核心工作线程失败,那就重新获取ctl,可能是线程池状态被其他线程修改了
        // 也可能是其他线程也在向线程池提交任务,导致核心工作线程已经超过了corePoolSize
        c = ctl.get();
    }
    
    // 线程池状态是否还是RUNNING,如果是就把任务添加到阻塞队列中
    if (isRunning(c) && workQueue.offer(command)) {
    
    
        
        // 在任务入队时,线程池的状态可能也会发生改变
        // 再次检查线程池的状态,如果线程池不是RUNNING了,那就不能再接受任务了,就得把任务从队列中移除,并进行拒绝策略
        
        // 如果线程池的状态没有发生改变,仍然是RUNNING,那就不需要把任务从队列中移除掉
        // 不过,为了确保刚刚入队的任务有线程会去处理它,需要判断一下工作线程数,如果为0,那就添加一个非核心的工作线程
        // 添加的这个线程没有自己的任务,目的就是从队列中获取任务来执行
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 如果线程池状态不是RUNNING,或者线程池状态是RUNNING但是队列满了,则去添加一个非核心工作线程
    // 实际上,addWorker中会判断线程池状态如果不是RUNNING,是不会添加工作线程的
    // false表示非核心工作线程,作用是,在addWorker内部会判断当前工作线程数已经超过了maximumPoolSize,如果超过了则会添加不成功,执行拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}

Método addWorker

O método addWorker é o método principal e é usado para adicionar threads. O parâmetro core indica se deve adicionar um thread principal ou um thread não principal.
Antes de examinar este método, podemos analisá-lo primeiro. O que é adicionar um thread?
Na verdade, você precisa iniciar um thread. Seja um thread principal ou um thread não principal, na verdade é apenas um thread comum. A diferença entre core e não core é: se você deseja adicionar um thread de trabalho
principal , você deve determinar o número atual de threads de trabalho. Se ele excede corePoolSize
. Caso contrário, um novo thread de trabalho será iniciado diretamente para executar a tarefa.
Se for excedido, um novo thread de trabalho não será iniciado, mas a tarefa será enfileirada.
Se você quiser adicionar um thread de trabalho não principal, deverá avaliar a situação atual. Se o número de threads de trabalho excede o máximoPoolSize.
Caso contrário, um novo thread de trabalho será iniciado para executar a tarefa diretamente.
Se for excedido, a tarefa será recusada para ser executada.
Portanto, no método addWorker, primeiro é necessário determinar se o thread de trabalho excedeu o limite. Se não exceder o limite, então Para iniciar um thread.

E no método addWorker, você deve determinar o status do pool de threads. Se o status do pool de threads não for RUNNING, então não há necessidade de adicionar threads. Claro, há um caso especial, ou seja, o o status do pool de threads é SHUTDOWN, mas na fila. Se houver uma tarefa, você ainda precisará adicionar um thread neste momento.

Então, como surgiu esse caso especial?

O que mencionamos anteriormente é iniciar um novo thread de trabalho, então como reciclar o thread de trabalho? É impossível que o thread de trabalho aberto esteja sempre ativo, porque se o número de tarefas diminuir de mais para menos, não haverá necessidade de muitos recursos de thread, então haverá um mecanismo no pool de threads para reciclar o iniciado thread de trabalho. Como reciclá-lo será discutido mais tarde. Como mencionado, vamos primeiro analisar se é possível que todos os threads no pool de threads tenham sido reciclados. A resposta é sim.

Em primeiro lugar, é compreensível que os threads de trabalho não essenciais sejam reciclados, mas os threads de trabalho principais deveriam ser reciclados? Na verdade, o significado da existência do pool de threads é gerar recursos de thread antecipadamente e usá-los diretamente quando os threads forem necessários. Não há necessidade de abrir temporariamente os threads. Portanto, em circunstâncias normais, os threads de trabalho principais abertos não precisam ser reciclados, mesmo que não estejam disponíveis temporariamente. Se a tarefa precisar ser processada, não há necessidade de reciclá-la. Apenas deixe o thread de trabalho principal aguardar lá.

mas! Existe um parâmetro no pool de threads:allowCoreThreadTimeOut, que indica se o thread de trabalho principal pode atingir o tempo limite, o que significa se o thread de trabalho principal pode ser reciclado.O parâmetro padrão é falso, mas podemos chamar permitirCoreThreadTimeOut(boolean value) para alterar esse parâmetro para true. , contanto que seja alterado, o thread de trabalho principal também será reciclado, portanto, todos os threads de trabalho no conjunto de threads poderão ser reciclados. Então, se todos os threads de trabalho forem reciclados, uma tarefa vem na fila de bloqueio, isso cria casos especiais.

Acho que você gosta

Origin blog.csdn.net/beautybug1126/article/details/132072664
Recomendado
Clasificación