1. Introdução multi-threading
Na programação, não podemos evitar problemas de programação com vários threading, porque o processamento simultâneo é necessário na maioria dos sistemas de negócios. Se estiver em cenários simultâneos, a multi-threading é muito importante. Além disso, durante a nossa entrevista, o entrevistador geralmente nos faz perguntas sobre o multi-threading, como: como criar um tópico? Geralmente respondemos dessa maneira, existem dois métodos principais. O primeiro é: herdar a classe Thread e reescrever o método de execução; O segundo é: implemente a interface executável e reescreva o método de execução. Em seguida, o entrevistador definitivamente perguntará quais são as vantagens e desvantagens desses dois métodos. Não importa o quê, chegaremos a uma conclusão, ou seja, a segunda maneira de uso, porque os defensores orientados a objetos e tentam usar combinações o máximo possível.
Neste momento, também podemos pensar no que fazer se queremos obter o valor de retorno dos multi-threads? Com base no conhecimento que aprendemos mais, pensaremos em implementar a interface chamada e reescrever o método de chamada. Como tantos tópicos são usados em projetos reais? De quantas maneiras eles têm?
Primeiro, vamos dar uma olhada em um exemplo:
Este é um método simples para criar vários threads, o que é fácil de entender. No exemplo, de acordo com diferentes cenários de negócios, podemos passar parâmetros diferentes para o thread () para implementar diferentes lógicas de negócios. No entanto, o problema exposto por esse método de criação de múltiplos threads é criar threads repetidamente e deve ser destruído após a criação de threads. Se os requisitos para cenários simultâneos forem baixos, esse método parece ser bom, mas em cenários de alta concorrência, esse método não é possível, porque a criação de threads é muito consumida por recursos. Portanto, de acordo com a experiência, a maneira correta é usar a tecnologia de threads pool. O JDK fornece uma variedade de tipos de pool de threads para escolhermos. Para métodos específicos, você pode verificar a documentação do JDK.
O que precisamos observar neste código é que os parâmetros passados representam o número de threads que configuramos. Quanto mais o melhor? Certamente não. Porque ao configurar o número de threads, devemos considerar completamente o desempenho do servidor. Se houver mais configurações de threads, o desempenho do servidor pode não ser excelente. Geralmente, os cálculos concluídos pela máquina são determinados pelo número de threads. Quando o número de encadeamentos atinge o pico, o cálculo não pode ser realizado. Se for a lógica de negócios que consome CPU (mais cálculos), o número de threads e núcleos atingirá seu pico. Se for a lógica de negócios que consome E/S (bancos de dados operacionais, carregando arquivos, download, etc.), quanto mais threads, mais threads, isso ajudará a melhorar o desempenho em um certo sentido.
Outra fórmula para definir o número de threads:
Y = n*((a+b)/a), onde n: o número de núcleos da CPU, a: o tempo de cálculo do programa quando o thread é executado, b: o tempo de bloqueio do programa quando o thread é executado. Com esta fórmula, a configuração da contagem de threads do pool de threads será restringida e podemos configurá -la com flexibilidade de acordo com a situação real da máquina.
2. Otimização multithread e comparação de desempenho
A tecnologia de encadeamento foi usada em projetos recentes e encontrei muitos problemas durante o uso. Aproveitando a popularidade, resolverei as comparações de desempenho de várias estruturas multithread. Os que dominamos são divididos aproximadamente em três tipos: o primeiro tipo: Threadpool (thread pool) + Countdownlatch (contador de programas), o segundo tipo: Framework/Framework e o terceiro tipo de fluxo paralelo JDK8. Aqui está um resumo comparativo do desempenho de múltiplos threading desses métodos.
Primeiro, assuma um cenário de negócios em que vários objetos de arquivo são gerados na memória. Aqui, 30.000 Thread Sleep está tentativamente determinado a simular a lógica de processamento de negócios para comparar o desempenho de vários threads desses métodos.
1) Único rosqueado
Esse método é muito simples, mas o programa consome muito tempo durante o processamento e será usado por um longo tempo, porque cada thread está aguardando a execução do thread atual antes de ser executado. Tem pouco a ver com vários threads, portanto a eficiência é muito baixa.
Primeiro, crie o objeto de arquivo, o código é o seguinte:
classe pública FileInfo {private string filename; // Nome do arquivo Private String fileType; // Tipo de arquivo Private String fileSize; // Tamanho do arquivo Private String Filemd5; // Código MD5 Private String FileVersionNo; // Número da versão do arquivo public FileInfo () {Super (); } public fileInfo (String FileName, String FileType, String fileSize, String filemd5, String FileVersionNo) {super (); this.Filename = FileName; this.FileType = FileType; this.Filesize = fileSize; this.filEMD5 = FILEMD5; this.FileversionNo = FILEVERSIONNO; } public string getFilename () {return filename; } public void setFilename (string filename) {this.filename = filename; } public string getFileType () {return filetype; } public void setFileType (string filetype) {this.FileType = FileType; } public String getFilesize () {return filesize; } public void setFilesize (string filesize) {this.filesize = filesize; } public string getFilEMD5 () {return filemd5; } public void setFilEMD5 (String filemd5) {this.filemd5 = filemd5; } public string getFileverSionNo () {return fileVersionNo; } public void setFileVersionNo (String fileVersionNo) {this.fileversionNo = FILEVERSIONNO; }Em seguida, simule o processamento de negócios, crie 30.000 objetos de arquivo, o encadeamento acomoda 1ms e define 1000ms antes e descobre que a hora é muito longa e todo o eclipse está preso, então mude o tempo para 1ms.
Public class Test {Private Static List <FileInfo> filElist = new ArrayList <FileInfo> (); public static void main (string [] args) lança interruptedException {createfileInfo (); long startTime = System.currenttimemillis (); para (FileInfo fi: FILELIST) {Thread.sleep (1); } long endtime = system.currenttimemillis (); System.out.println ("Thread único demorado:"+(Endtime-starttime)+"ms"); } private estático void createfileInfo () {for (int i = 0; i <30000; i ++) {filelist.add (new FileInfo ("foto frontal do cartão de identificação", "jpg", "101522", "md5"+i, "1");); }}}Os resultados dos testes são os seguintes:
Pode -se observar que a geração de 30.000 objetos de arquivo leva muito tempo, quase 1 minuto, e a eficiência é relativamente baixa.
2) Threadpool (pool de threads) +Countdownlatch (contador de programas)
Como o nome sugere, o CountdownLatch é um contador de threads. Seu processo de execução é o seguinte: Primeiro, o método Wait () é chamado no encadeamento principal e o encadeamento principal é bloqueado e, em seguida, o contador do programa é passado para o objeto Thread como um parâmetro. Finalmente, depois que cada thread termina de executar a tarefa, o método Countdown () é chamado para indicar a conclusão da tarefa. Depois que a Countdown () é executada várias vezes, o thread principal aguarda () será inválido. O processo de implementação é o seguinte:
classe pública Test2 {private Static ExecorService Exector = executores.newfixedThreadpool (100); CountDownLatch privado de CountdownLatchLatch = new Countdownlatch (100); Lista estática privada <FileInfo> filelist = new ArrayList <FileInfo> (); Lista estática privada <List <FileInfo>> List = new ArrayList <> (); public static void main (string [] args) lança interruptedException {createfileInfo (); addList (); long startTime = System.currenttimemillis (); int i = 0; for (list <FileInfo> fi: list) {Execoror.subMit (new Filerunnable (CountdownLatch, fi, i)); i ++; } Countdownlatch.await (); Long Endtime = System.CurrentTimemillis (); executor.shutdown (); System.out.println (i+"threads levam tempo:"+(Endtime-starttime)+"ms"); } private estático void createfileInfo () {for (int i = 0; i <30000; i ++) {filelist.add (new FileInfo ("foto do cartão da frente", "jpg", "101522", "md5"+i, "1")); }} private estático void addList () {for (int i = 0; i <100; i ++) {list.add (filelist); }}}Classe Filerunnable:
/** * Processamento multithread * @author wangsj * * @param <T> */public class Filerunnable <T> implementa Runnable {private controDdownLatch Countdownlatch; Lista privada <T> Lista; privado int i; public filerunnable (CountdownLatch Countdownlatch, List <T> List, int i) {super (); this.CountDownLatch = CountDownLatch; this.list = list; this.i = i; } @Override public void run () {for (t t: list) {try {thread.sleep (1); } catch (interruptedException e) {e.printStackTrace (); } Countdownlatch.CountDown (); }}}Os resultados dos testes são os seguintes:
3) Forek/junção da estrutura
O JDK começou com a versão 7, e a estrutura do garfo/junção apareceu. De uma perspectiva literal, o Fork está se dividindo e a junção é a fusão, então a idéia dessa estrutura é. Divida a tarefa através do garfo e junte -se para mesclar os resultados depois que os caracteres divididos são executados e resumidos. Por exemplo, queremos calcular vários números que são adicionados continuamente, 2+4+5+7 =? , Como usamos a estrutura do garfo/junção para concluí -lo? A idéia é dividir as tarefas moleculares. Podemos dividir essa operação em duas subtarefas, uma calcula 2+4 e a outra calcula 5+7. Este é o processo de garfo. Após a conclusão do cálculo, os resultados do cálculo dessas duas subtarefas são resumidos e a soma é obtida. Este é o processo de junção.
IDEA DE EXECUÇÃO DO FROCTOWORK FROCTO/JONE: Primeiro, divida as tarefas e use a classe Fork para dividir grandes tarefas em várias subtaretas. Esse processo de segmentação precisa ser determinado de acordo com a situação real até que as tarefas divididas sejam pequenas o suficiente. Em seguida, a classe de junção executa a tarefa e as subtarefas divididas estão em diferentes filas. Vários tópicos obtêm tarefas da fila e as executam. Os resultados da execução são colocados em uma fila separada. Finalmente, o encadeamento é iniciado, os resultados são obtidos na fila e os resultados são mesclados.
Várias classes são usadas para usar a estrutura do garfo/junção. Para o uso da classe, você pode consultar a API JDK. Usando essa estrutura, você precisa herdar a classe ForkJointask. Geralmente, você só precisa herdar sua subclasse Recursivetask ou Recursiveation. O recursivetask é usado para cenas com resultados de retorno, e o Recursiveaction é usado para cenas sem resultados de retorno. A execução do Forkjointask requer a execução do ForkJoinpool, que é usado para manter as subtarefas divididas adicionadas a diferentes filas de tarefas.
Aqui está o código de implementação:
classe pública Test3 {Private Static List <FileInfo> filElist = new ArrayList <FileInfo> (); // private estático forkjoinpool forkJoinpool = new ForkJoinpool (100); // Trabalho estático privado <FileInfo> Job = New Job <> (Filelist.size ()/100, FILLIST); public static void main (string [] args) {createfileInfo (); long startTime = System.currenttimemillis (); Forkjoinpool forkjoinpool = new forkjoinpool (100); // dividiu o trabalho de tarefa <FileInfo> Job = novo trabalho <> (filelist.size ()/100, fileList); // Envie a tarefa e retorne o resultado ForkJointask <Integer> fjtResult = forkjoinpool.submit (Job); // bloqueia while (! Job.isdone ()) {System.out.println ("Tarefa concluída!"); } long endtime = system.currenttimemillis (); System.out.println ("Fork/JONE Framework demorando tempo:"+(Endtime-startTime)+"MS"); } private estático void createfileInfo () {for (int i = 0; i <30000; i ++) {filelist.add (new FileInfo ("foto do cartão da frente", "jpg", "101522", "md5"+i, "1")); }}}/ * * Parte INT privada; Lista privada <T> JoBlist; trabalho público (int conting, list <t> joblist) {super (); this.count = count; this.Joblist = JoBlist; } /*** Execute a tarefa, semelhante ao método de execução que implementa a interface executável* /@Override Protected integer compute () {// dividir a tarefa if (joBlist.size () <= count) {ExecuteJob (); return joBlist.size (); } else {// continua a criar a tarefa até que ela possa ser decomposta e executada lista <Recursivetask <long>> fork = new LinkedList <RecursiveTask <long>> (); // Dividir a tarefa nucleica, aqui o método de dicotomia é usado int CountJob = joBlist.size ()/2; List <T> LeftList = JoBlist.sublist (0, CountJob); List <T> RightList = JoBlist.Sublist (CountJob, JoBlist.size ()); // atribui tarefas Job LeftJob = New Job <> (contagem, Lista de esquerda); Job RightJob = New Job <> (Conde, RightList); // execute a tarefa leftJob.fork (); RightJob.fork (); Retorne Integer.parseint (leftJob.join (). ToString ()) +Integer.parseint (RightJob.join (). ToString ()); }} / *** Execute o método da tarefa* / private void executejob () {for (t job: joblist) {try {thread.sleep (1); } catch (interruptedException e) {e.printStackTrace (); }}}Os resultados dos testes são os seguintes:
4) Streaming paralelo JDK8
O fluxo paralelo é um dos novos recursos do JDK8. A idéia é transformar um fluxo executado sequencialmente em um fluxo simultâneo, que é implementado chamando o método parallel (). O fluxo paralelo divide um fluxo em vários blocos de dados, usa encadeamentos diferentes para processar os fluxos de diferentes blocos de dados e, finalmente, mescla os resultados do processamento de cada bloco de fluxo de dados, semelhante à estrutura do garfo/junção.
O fluxo paralelo usa o pool de threads públicos ForkJoinpool por padrão. O número de threads é o valor padrão usado. De acordo com o número de núcleos da máquina, podemos ajustar o tamanho dos encadeamentos adequadamente. Ajustar o número de encadeamentos é alcançado das seguintes maneiras.
System.setProperty ("java.util.concurrent.forkjoinpool.common.parallelalism", "100");A seguir, o processo de implementação do código, que é muito simples:
classe pública Test4 {Private Static List <FileInfo> filElist = new ArrayList <FileInfo> (); public static void main (string [] args) {// System.setProperty ("java.util.concurrent.forkjoinpool.common.parallelelism", "100"); createfileInfo (); long startTime = System.currenttimemillis (); filelist.paralLelsTream (). foreach (e -> {tente {thread.sleep (1);} catch (interruptedException f) {f.printStackTrace ();}}); Long Endtime = System.CurrentTimemillis (); System.out.println ("JDK8 Parallel Streaming Time:"+(Endtime-startTime)+"ms");} private estático void createfileInfo () {for (int i = 0; i <30000; i ++) {filelist.add (new FileInfo ("Front of Id cartão "," jpg "," 101522 "," md5 "+i," 1 ")); }}}O seguinte é o teste. O número de pools de threads não está definido pela primeira vez. O padrão é usado. Os resultados dos testes são os seguintes:
Vimos que o resultado não é muito ideal e leva muito tempo. Em seguida, defina o número de pools de threads, ou seja, adicione o seguinte código:
System.setProperty ("java.util.concurrent.forkjoinpool.common.parallelalism", "100");Em seguida, o teste foi realizado e os resultados foram os seguintes:
Desta vez, leva menos tempo e é ideal.
3. Resumo
Para resumir as situações acima, usando um único thread como referência, o mais longo tempo consome a estrutura de garfo/junção nativa. Embora o número de pools de threads esteja configurado aqui, o fluxo paralelo do JDK8 com o número de pools de threads é mais pobre. O streaming paralelo implementa o código é simples e fácil de entender, e não precisamos escrever mais para loops. Podemos concluir todos os métodos paralelos e a quantidade de código é bastante reduzida. De fato, a camada subjacente do streaming paralela ainda é a estrutura do garfo/junção, que exige que usemos várias tecnologias durante o processo de desenvolvimento para distinguir as vantagens e desvantagens de várias tecnologias, para melhor nos servir.