Java ~ ForkJoinPool + parallelStream realiza processamento de fluxo de dados paralelo e rápido

ForkJoinPool

Falando em ForkJoinPool, primeiro fale sobre o framework Fork / Join

Usamos as palavras Fork e Join para entender a estrutura Fork / Join. Fork é dividir uma grande tarefa em várias subtarefas para execução paralela, Join é para mesclar os resultados da execução dessas subtarefas e, finalmente, obter o resultado dessa grande tarefa. Por exemplo, o cálculo de 1 + 2 +… + 10000 pode ser dividido em 10 subtarefas, e cada subtarefa soma 1.000 números separadamente e, finalmente, resume os resultados dessas 10 subtarefas.
Insira a descrição da imagem aqui
Cada thread bifurcará as subtarefas divididas por Coloque as subtarefas em suas próprias deque de bloqueio de thread-safe e, em seguida, os threads obterão a execução da tarefa da cabeça do deque. Os resultados da execução das subtarefas são todos colocados em uma fila, um thread é iniciado para obter dados da fila e, em seguida, os dados são mesclados.

Colocar tarefas na fila de duas pontas é implementar o algoritmo de roubo de trabalho, que se refere a tarefas de roubo de thread de outras filas para execução. Então, por que você precisa usar algoritmos de roubo de trabalho? Se precisarmos fazer uma tarefa relativamente grande, podemos dividir essa tarefa em várias subtarefas independentes. Para reduzir a competição entre os threads, coloque essas subtarefas em filas diferentes e crie uma para cada fila. Um thread separado executa as tarefas no fila, e o encadeamento e a fila correspondem um a um. Por exemplo, o thread A é responsável pelo processamento das tarefas na fila A. No entanto, alguns encadeamentos concluirão as tarefas em suas próprias filas primeiro, enquanto ainda há tarefas aguardando para serem processadas nas filas correspondentes a outros encadeamentos. Em vez de esperar, o encadeamento que termina seu trabalho também pode ajudar outros encadeamentos a funcionarem, então ele vai para a fila de outros encadeamentos para roubar uma tarefa para execução. Nesse momento, eles acessarão a mesma fila, portanto, para reduzir a competição entre o encadeamento de tarefa roubado e o encadeamento de tarefa roubada, uma fila dupla é geralmente usada. O encadeamento de tarefa roubado sempre executa a tarefa do chefe de a fila de pontas duplas. O thread que rouba a tarefa sempre a executa a partir do final do deque.

A vantagem deste algoritmo é que ele melhora a eficiência. A
desvantagem é que há consumo excessivo. Por exemplo, quando há apenas uma tarefa, um thread pode ter terminado, mas você tem que ir para outros threads para dar uma olhada e cumprimentar isto.

ForkJoinTask: Se quisermos usar a estrutura ForkJoin, devemos primeiro criar uma tarefa ForkJoin. Ele fornece um mecanismo para realizar operações fork () e join () em tarefas. Normalmente, não precisamos herdar diretamente a classe ForkJoinTask, mas apenas herdar suas subclasses. A estrutura Fork / Join fornece as duas subclasses a seguir:

RecursiveAction: usado para tarefas que não retornam resultados.
RecursiveTask: usado para tarefas que retornam resultados.

A vantagem do ForkJoinPool é que ele pode fazer uso total das vantagens da CPU multi-cpu e multi-core, dividir uma tarefa em várias "pequenas tarefas" e colocar várias "pequenas tarefas" em vários núcleos de processador para execução paralela; a execução das "pequenas tarefas" é concluída, esses resultados de execução podem ser combinados.

Java 8 adiciona um conjunto de encadeamentos geral ao ForkJoinPool, que é usado para lidar com tarefas que não foram explicitamente enviadas a nenhum conjunto de encadeamentos. É um elemento estático do tipo ForkJoinPool e o número padrão de threads que possui é igual ao número de processadores no computador em execução.

ps: Durante a execução do ForkJoinPool, um grande número de subtarefas serão criadas, o que fará com que o GC execute a coleta de lixo, estas precisam ser atentadas, portanto, você deve prestar atenção a elas ao usá-las.

Construtor

public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
    
    
        this(checkParallelism(parallelism),
             checkFactory(factory),
             handler,
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
             "ForkJoinPool-" + nextPoolId() + "-worker-");
        checkPermission();
    }
  • Paralelismo: o grau de paralelismo, o padrão é o número de CPUs, o mínimo é 1
  • fábrica: fábrica de fios de trabalho;
  • manipulador: lida com a classe de situação anormal quando o thread de trabalho executa a tarefa, o padrão é nulo;
  • asyncMode: Seja no modo assíncrono, o padrão é falso. Se verdadeiro, significa que a execução de subtarefas segue a ordem FIFO e as tarefas não podem ser unidas (unir). Este modo é adequado para threads de trabalho executarem apenas tarefas assíncronas de tipo de evento.

Na maioria dos cenários, se não houver um forte requisito de negócios, geralmente usamos o pool comum em ForkJoinPool diretamente. Após JDK 1.8, o método ForkJoinPool.commonPool () pode ser usado diretamente para usar o pool comum. Dê uma olhada em sua estrutura :

piscina comum()

private static ForkJoinPool makeCommonPool() {
    
    
    int parallelism = -1;
    ForkJoinWorkerThreadFactory factory = null;
    UncaughtExceptionHandler handler = null;
    try {
    
      // ignore exceptions in accessing/parsing
        String pp = System.getProperty
                ("java.util.concurrent.ForkJoinPool.common.parallelism");//并行度
        String fp = System.getProperty
                ("java.util.concurrent.ForkJoinPool.common.threadFactory");//线程工厂
        String hp = System.getProperty
                ("java.util.concurrent.ForkJoinPool.common.exceptionHandler");//异常处理类
        if (pp != null)
            parallelism = Integer.parseInt(pp);
        if (fp != null)
            factory = ((ForkJoinWorkerThreadFactory) ClassLoader.
                    getSystemClassLoader().loadClass(fp).newInstance());
        if (hp != null)
            handler = ((UncaughtExceptionHandler) ClassLoader.
                    getSystemClassLoader().loadClass(hp).newInstance());
    } catch (Exception ignore) {
    
    
    }
    if (factory == null) {
    
    
        if (System.getSecurityManager() == null)
            factory = defaultForkJoinWorkerThreadFactory;
        else // use security-managed default
            factory = new InnocuousForkJoinWorkerThreadFactory();
    }
    if (parallelism < 0 && // default 1 less than #cores
            (parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0)
        parallelism = 1;//默认并行度为1
    if (parallelism > MAX_CAP)
        parallelism = MAX_CAP;
    return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE,
            "ForkJoinPool.commonPool-worker-");
}

A vantagem de usar o pool comum é que podemos definir "paralelismo, fábrica de threads e classe de tratamento de exceções" especificando os parâmetros do sistema ; e usa um modo síncrono, o que significa que pode oferecer suporte à mesclagem de tarefas (junção).

A diferença entre invocar, executar e enviar

Ao usar ForkJoinPool, descobri que os métodos para realizar tarefas são:

invoke (tarefa ForkJoinTask)
execute ( tarefa ForkJoinTask <?>)
submit (tarefa ForkJoinTask)

A diferença entre enviar e executar e invocar é externalPush (tarefa); não haverá task.join no futuro

A função desta chamada de método de junção é fazer com que o thread principal suspenda à espera do resultado da tarefa.

execute (ForkJoinTask) Execução assíncrona de tarefas, nenhum valor de retorno
invocado (ForkJoinTask) Join fará com que o thread principal suspenda a espera pelo resultado da tarefa, as tarefas serão sincronizadas para o processo principal
enviar (ForkJoinTask) execução assíncrona, retornar a tarefa objeto diretamente, através da tarefa .get / join bloqueia o thread principal e sincroniza o resultado com o thread principal

paralelismo

Além do novo fluxo, java8 também fornece uma versão de fluxo paralelo multi-threaded do fluxo. A vantagem do fluxo paralelo é: fazer uso total de multi-threading e melhorar a eficiência da operação do programa, mas o uso correto não é o uso simples e cego pode levar às seguintes consequências

  1. A eficiência não aumenta, mas diminui
  2. Adicione mais complexidade, o programa é mais sujeito a erros

A eficiência não aumenta, mas diminui.
O fluxo paralelo é baseado na estrutura fork / join. Simplificando, é feito usando vários threads. Ao usar o fluxo paralelo, considere o tempo para inicializar a estrutura fork / join, ou seja, deve haver chegar a hora de inicializar o thread. A tarefa a ser realizada é muito simples, então o tempo para inicializar o framework fork / join será muito maior do que o tempo necessário para executar a tarefa, o que leva a uma diminuição na eficiência. explicação do apêndice doug Lee, o número de tarefas * o número de linhas do método de execução> = 10000 ou é necessário realizar operações que consomem muito tempo (como io / banco de dados)

Com complexidade adicional, o programa é mais sujeito a erros e
haverá problemas de segurança multi-thread

Perceba o processamento rápido do fluxo de dados

    public static void main(String[] args) throws InterruptedException {
    
    
        List<Integer> ids = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
    
    
            ids.add(i);
        }

        ForkJoinPool pool = new ForkJoinPool(10);
        List<String> result = new ArrayList<>();
        pool.submit(() -> ids.parallelStream().forEach(id -> {
    
    
            id += 1;
            result.add(String.valueOf(id));
        })).join();

        Thread.sleep(1000);
        pool.shutdown();
        System.out.println(result.size());
    }

Mas o código acima é problemático, ou seja, um problema de multithreading

Porque estou usando uma arrayList comum, se houver problemas de segurança de thread em multi-threading, ou seja, problemas de perda de dados
Insira a descrição da imagem aqui

Solução

  1. Use uma ArrayList segura, como as três seguintes
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        List<String> list1 = new CopyOnWriteArrayList<>();
        List<String> list2 = new Vector<>();
  1. Transforme a operação de gravação na lista vinculada em um bloco de código síncrono
    public static void main(String[] args) throws InterruptedException {
    
    

        List<String> list = Collections.synchronizedList(new ArrayList<>());
        List<String> list1 = new CopyOnWriteArrayList<>();
        List<String> list2 = new Vector<>();

        List<Integer> ids = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
    
    
            ids.add(i);
        }

        ForkJoinPool pool = new ForkJoinPool(10);
        List<String> result = new ArrayList<>();
        pool.submit(() -> ids.parallelStream().forEach(id -> {
    
    
            id += 1;
            synchronized (pool) {
    
    
                result.add(String.valueOf(id));
            }
        })).join();

        Thread.sleep(1000);
        pool.shutdown();
        System.out.println(result.size());
    }

Acho que você gosta

Origin blog.csdn.net/Shangxingya/article/details/114682297
Recomendado
Clasificación