Independentemente de você estar seguindo ou não, os aplicativos da Web Java usam pools de threads para lidar com solicitações em maior ou menor grau. Os detalhes da implementação dos pools de threads podem ser ignorados, mas é necessário entender mais cedo ou posterior no uso e ajuste dos pools de threads. Este artigo apresenta principalmente o uso do Java Thread Pool e como configurar corretamente o pool de threads.
Único rosqueado
Vamos começar com o básico. Não importa qual servidor de aplicativos ou estrutura (como Tomcat, Jetty etc.) são usados, eles têm implementações básicas semelhantes. A base de um serviço da Web é um soquete, responsável por ouvir a porta, aguardar a conexão TCP e aceitar a conexão TCP. Depois que a conexão TCP é aceita, os dados podem ser lidos e enviados da conexão TCP recém -criada.
Para entender o processo acima, não usamos nenhum servidor de aplicativos diretamente, mas criamos um serviço da Web simples a partir do zero. Este serviço é um microcosmo da maioria dos servidores de aplicativos. Um simples serviço da web de thread único se parece com o seguinte:
Listener ServerSocket = new ServerSocket (8080); tente {while (true) {soquete soket = listener.accept (); tente {handlerequest (soquete); } catch (ioexception e) {e.printStackTrace (); }}} finalmente {lidener.close ();}O código acima cria um soquete do servidor (ServerSocket) , ouve na porta 8080 e depois faz um loop para verificar o soquete para ver se há uma nova conexão. Depois que uma nova conexão for aceita, o soquete será passado para o método HandleRequest. Este método analisa o fluxo de dados em uma solicitação HTTP, responde e grava os dados de resposta. Neste exemplo simples, o método HandleRequest simplesmente implementa a leitura do fluxo de dados e retorna um dados de resposta simples. Em implementações gerais, esse método será muito mais complexo, como a leitura de dados de um banco de dados, etc.
Resposta final da string estática = "HTTP/1.0 200 OK/R/N" + "Tipo de conteúdo: Text/Plain/R/N" + "/R/N" + "Hello World/R/N"; public static void handlerequest (soquete) lança ioexception {// leia o fluxo de entrada e retorne "200 ok", tente {bufferredReader in = new BufferReader (new InputStreamReader (soket.getInputInputStream ()); log.info (in.readline ()); OutputStream out = Socket.getOutputStream (); out.Write (Response.getBytes (StandardCharsets.utf_8)); } finalmente {Socket.close (); }}Como existe apenas um thread para processar a solicitação, cada solicitação deve aguardar que a solicitação anterior seja processada antes de poder ser respondida. Supondo que um tempo de resposta de solicitação seja de 100 milissegundos, o número de respostas por segundo (TPS) deste servidor é de apenas 10.
Multi-thread
Embora o método HandleRequest possa bloquear o IO, a CPU ainda pode lidar com mais solicitações. Mas em um único caso de rosca, isso não pode ser feito. Portanto, o recurso de processamento paralelo do servidor pode ser aprimorado criando métodos de threading com vários threading.
Classe estática pública handlerequestrunnable implementa Runnable {final Socket Socket; public HandleRequestRunnable (soquete) {this.socket = soket; } public void run () {try {handlerequest (soquete); } catch (ioexception e) {e.printStackTrace (); }}} Serversocket ouvinte = new ServerSocket (8080); tente {while (true) {soquete soquete = ouvinte.accept (); novo thread (novo handlerequestrunnable (soquete)). start (); }} finalmente {listener.close ();}Aqui, o método aceit () ainda é chamado no thread principal, mas uma vez que a conexão TCP for estabelecida, um novo thread será criado para lidar com a nova solicitação, que é executar o método Handlerequest no texto anterior no novo thread.
Ao criar um novo thread, o encadeamento principal pode continuar aceitando novas conexões TCP, e essas solicitações podem ser processadas em paralelo. Este método é chamado de "um thread por solicitação". Obviamente, existem outras maneiras de melhorar o desempenho do processamento, como o modelo assíncrono orientado a eventos usado pelo Nginx e Node.js, mas eles não usam pools de threads e, portanto, não são cobertos por este artigo.
Em cada solicitação, a implementação de um thread, a criação de um thread (e a destruição subsequente) é muito caro porque a JVM e o sistema operacional precisam alocar recursos. Além disso, a implementação acima também tem um problema, ou seja, o número de threads criados é incontrolável, o que pode fazer com que os recursos do sistema sejam esgotados rapidamente.
Recursos exaustos
Cada encadeamento requer uma certa quantidade de espaço de memória de pilha. Na JVM de 64 bits mais recente, o tamanho da pilha padrão é de 1024kb. Se o servidor receber um grande número de solicitações ou o método HandleRequest será executado lentamente, o servidor poderá travar devido à criação de um grande número de threads. Por exemplo, existem 1000 solicitações paralelas e os 1000 threads criados precisam usar 1 GB de memória JVM como espaço de pilha de threads. Além disso, os objetos criados durante a execução do código de cada thread também podem ser criados na pilha. Se essa situação piorar, excederá a memória da JVM Heap e gerará uma grande quantidade de operações de coleta de lixo, o que acabará causando transbordamento de memória (OrofMemoryErrors).
Esses threads não apenas consomem memória, mas também usam outros recursos limitados, como alças de arquivo, conexões de banco de dados, etc. Tópicos de criação incontroláveis também podem causar outros tipos de erros e falhas. Portanto, uma maneira importante de evitar a exaustão de recursos é evitar estruturas de dados incontroláveis.
A propósito, devido a problemas de memória causados pelo tamanho da pilha de threads, o tamanho da pilha pode ser ajustado através do comutador -xss. Depois de reduzir o tamanho da pilha do fio, a sobrecarga por rosca pode ser reduzida, mas o transbordamento da pilha (StackOverFlowerRors) pode ser aumentado. Para aplicações gerais, o padrão de 1024kb é muito rico e pode ser mais apropriado reduzi -lo para 256kb ou 512kb. O valor mínimo permitido em Java é de 160kb.
Pool de threads
Para evitar a criação contínua de novos threads, você pode limitar o limite superior do pool de threads usando um pool simples de threads. O pool de threads gerencia todos os threads. Se o número de roscas não atingir o limite superior, o pool de encadeamentos cria roscas para o limite superior e reutiliza o máximo possível roscas livres.
Serversocket Listener = new ServerSocket (8080); ExecutorService Execor (Executores.NewfixedThreadpool (4); tente {while (true) {soquete soket = listener.accept (); Executor.subMit (new Handlerequestrunnable (soquete)); }} finalmente {listener.close ();}Neste exemplo, em vez de criar o thread diretamente, o ExecorService é usado. Ele envia as tarefas que precisam ser executadas (precisam implementar a interface Runnables) para o pool de threads e executa o código usando threads no pool de threads. No exemplo, um pool de threads de tamanho fixo com vários threads de 4 é usado para processar todas as solicitações. Isso limita o número de encadeamentos que lidam com solicitações e também limita o uso de recursos.
Além de criar um pool de threads de tamanho fixo através do método NewFixedThreadpool, a classe Executores também fornece o método NewCachedThreadpool. A reutilização de um pool de threads ainda pode levar a um número incontrolável de threads, mas usará os encadeamentos ociosos que foram criados antes o máximo possível. Geralmente, esse tipo de pool de threads é adequado para tarefas curtas que não são bloqueadas por recursos externos.
Fila de trabalho
Depois de usar um pool de threads de tamanho fixo, se todos os threads estiverem ocupados, o que acontecerá se outra solicitação ocorrer? O ThreadpoolExecutor usa uma fila para manter solicitações pendentes e os pools de threads de tamanho fixo usam listas vinculadas ilimitadas por padrão. Observe que isso, por sua vez, pode causar problemas de exaustão de recursos, mas isso não acontecerá enquanto a velocidade de processamento de roscas for maior que a taxa de crescimento da fila. Então, no exemplo anterior, cada solicitação na fila manterá um soquete que em alguns sistemas operacionais consumirá o identificador de arquivo. Como o sistema operacional limita o número de alças de arquivo abertas pelo processo, é melhor limitar o tamanho da fila de trabalho.
public Static ExecorsService NewboundfixedThreadpool (int nthreads, int Capacidade) {Retorne novo ThreadPoolExecutor (NTHRADS, NTHREADS, 0L, TimeUnit.millisEconds, novo linkdblockQueue <drunnable> (Capacidade), ThreadPoolExector.discardPolicy (); Ioexception {serversocket ouvinte = new ServerSocket (8080); ExecutorService Executor = newboudfixedThreadpool (4, 16); tente {while (true) {soquete soket = ouvinte.accept (); Executor.subMit (new Handlerequestrunnable (soquete)); }} finalmente {ouvinte.close (); }}Aqui, em vez de usar diretamente o método executores.newfixedThreadpool para criar um pool de threads, construímos o threadpoolExecutor Objetivo e limitamos o comprimento da fila de trabalho a 16 elementos.
Se todos os tópicos estiverem ocupados, a nova tarefa será preenchida na fila. Como a fila limita o tamanho a 16 elementos, se esse limite for excedido, ele precisará ser tratado pelo último parâmetro ao construir o objeto ThreadPoolExecutor. No exemplo, é usada uma colméia de descarte, ou seja, quando a fila atingir o limite superior, a nova tarefa será descartada. Além da primeira vez, também há uma política de aborto (abortpolicy) e uma política de execução de chamadas (CallerNSpolicy). O primeiro lançará uma exceção, enquanto o último executará a tarefa no encadeamento do chamador.
Para aplicativos da Web, a política padrão ideal deve ser abandonar ou abortar a política e retornar um erro ao cliente (como um erro HTTP 503). Obviamente, também é possível evitar abandonar as solicitações de clientes, aumentando a duração da fila de trabalho, mas as solicitações de usuário geralmente não estão dispostas a esperar por um longo tempo, e isso consumirá mais recursos do servidor. O objetivo da fila de trabalho não é responder às solicitações do cliente sem limite, mas para suavizar e estourar solicitações. Normalmente, a fila de trabalho deve estar vazia.
Ajuste da contagem de threads
O exemplo anterior mostra como criar e usar um pool de threads, mas o problema principal do uso de um pool de threads é quantos threads devem ser usados. Primeiro, precisamos garantir que, quando o limite do encadeamento for atingido, o recurso não será esgotado. Os recursos aqui incluem memória (heap e pilha), número de alças de arquivo aberto, número de conexões TCP, número de conexões de banco de dados remotos e outros recursos limitados. Em particular, se as tarefas encadeadas forem computacionalmente intensivas, o número de núcleos da CPU também é uma das limitações de recursos. De um modo geral, o número de threads não deve exceder o número de núcleos da CPU.
Como a seleção da contagem de roscas depende do tipo de aplicação, pode ser necessário muito teste de desempenho antes que os resultados ideais possam ser obtidos. Obviamente, você também pode melhorar o desempenho do seu aplicativo aumentando o número de recursos. Por exemplo, modifique o tamanho da memória JVM Heap ou modifique o limite superior do identificador de arquivo do sistema operacional, etc. Em seguida, esses ajustes acabarão atingindo o limite superior teórico.
Lei de Little
A lei de Little descreve a relação entre três variáveis em um sistema estável.
Onde l representa o número médio de solicitações, λ representa a frequência das solicitações e W representa o tempo médio para responder à solicitação. Por exemplo, se o número de solicitações por segundo for 10 e cada tempo de processamento de solicitação for 1 segundo, a qualquer momento, há 10 solicitações sendo processadas. De volta ao nosso tópico, requer 10 threads para processar. Se o tempo de processamento de uma única solicitação dobrar, o número de threads processados também dobrará, ficando 20.
Depois de entender o impacto do tempo de processamento na eficiência do processamento de solicitação, descobriremos que o limite superior teórico pode não ser o valor ideal para o tamanho do pool de encadeamentos. O limite superior do pool de threads também requer um tempo de processamento de tarefas de referência.
Supondo que a JVM possa processar 1000 tarefas em paralelo, se cada tempo de processamento de solicitação não exceder 30 segundos, então, no pior caso, no máximo 33,3 solicitações por segundo poderá ser processado. No entanto, se cada solicitação levar apenas 500 milissegundos, o aplicativo poderá processar 2000 solicitações por segundo.
Pool de threads dividido
Em microsserviços ou arquiteturas orientadas a serviços (SOA), geralmente é necessário acesso a vários serviços de back-end. Se um dos serviços executar o desempenho degradado, poderá fazer com que o pool de threads fique sem threads, o que afeta solicitações a outros serviços.
Uma maneira eficaz de lidar com a falha do serviço de back -end é isolar o pool de threads usado por cada serviço. Nesse modo, ainda existe um pool de threads despachado que envia tarefas para diferentes pools de threads de solicitação de back -end. Este pool de threads pode não ter carga por causa de um back -end lento e transferir a carga para um pool de threads que solicita back -end lento.
Além disso, o modo de agrupamento com vários threads também precisa evitar problemas de impasse. Se cada thread estiver bloqueando enquanto aguarda o resultado de uma solicitação não processada, ocorre um impasse. Portanto, no modo de pool multithread, é necessário entender as tarefas executadas por cada pool de threads e as dependências entre elas, de modo a evitar problemas de impasse o máximo possível.
Resumir
Mesmo que os pools de threads não sejam usados diretamente no aplicativo, é provável que eles sejam usados indiretamente pelo servidor de aplicativos ou estrutura no aplicativo. Estruturas como Tomcat, JBoss, Underlow, DropWizard, etc. Todos fornecem opções para ajustar os pools de threads (pools de threads usados pela execução do servlet).
Espero que este artigo possa melhorar sua compreensão do pool de threads e ajudá -lo a aprender.