Como usar streams paralelos com eficiência

Antes do Java7, era difícil processar uma grande quantidade de dados em paralelo. Primeiro, divida os dados em várias partes, depois coloque essas subpartes em cada thread para executar a lógica de cálculo e, por fim, retorne os dados de cada thread. Os resultados do cálculo são mesclados; uma estrutura fork / join para processamento de big data é fornecida em Java7, que protege o processamento interativo entre threads e se concentra mais no processamento de dados.


Estrutura Fork / Join

O framework Fork / Join adota a ideia de dividir e conquistar, dividir grandes tarefas em pequenas tarefas e, em seguida, colocá-las em threads independentes para cálculo. Ao mesmo tempo, a fim de maximizar o uso de CPUs multi-core, 工作窃取vários algoritmos são usados ​​para executar Tarefas, isto é, depois que um thread processou as tarefas em sua própria fila de trabalho, ele tenta roubar uma tarefa da fila de trabalho de outros threads para execução até que todas as tarefas sejam processadas. Portanto, a fim de reduzir a competição entre os threads, geralmente é usado um deque. O thread de tarefa roubado sempre tira a tarefa do início do deque para execução, e a tarefa de roubo de thread sempre tira a tarefa do final do deque para execução; no Baidu Encontrou uma foto

imagem

  • Para usar a estrutura RecursiveTask
    Fork / Join, você primeiro precisa criar suas próprias tarefas, herdar RecursiveTaske implementar métodos abstratos
protected abstract V compute();

A classe de implementação precisa dividir, calcular e mesclar tarefas neste método; o pseudocódigo pode ser expresso da seguinte maneira:

if(任务已经不可拆分){
    return 顺序计算结果;
} else {
    1.任务拆分成两个子任务
    2.递归调用本方法,拆分子任务
    3.等待子任务执行完成
    4.合并子任务的结果
}
  • Fork / Join combat

Tarefa: Complete a soma de 100 milhões de números naturais

Primeiro usamos o método serial para conseguir, o código é o seguinte:

long result = LongStream.rangeClosed(1, 100000000)
                .reduce(0, Long::sum);
System.out.println("result:" + result);

Use a estrutura Fork / Join para conseguir, o código é o seguinte:

public class SumRecursiveTask extends RecursiveTask<Long> {
    private long[] numbers;
    private int start;
    private int end;

    public SumRecursiveTask(long[] numbers) {
        this.numbers = numbers;
        this.start = 0;
        this.end = numbers.length;
    }

    public SumRecursiveTask(long[] numbers, int start, int end) {
        this.numbers = numbers;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        int length = end - start;
        if (length < 20000) {  //小于20000个就不在进行拆分
            return sum();
        }
        SumRecursiveTask leftTask = new SumRecursiveTask(numbers, start, start + length / 2); //进行任务拆分
        SumRecursiveTask rightTask = new SumRecursiveTask(numbers, start + (length / 2), end); //进行任务拆分
        leftTask.fork(); //把该子任务交友ForkJoinPoll线程池去执行
        rightTask.fork(); //把该子任务交友ForkJoinPoll线程池去执行
        return leftTask.join() + rightTask.join(); //把子任务的结果相加
    }

    private long sum() {
        int sum = 0;
        for (int i = start; i < end; i++) {
            sum += numbers[i];
        }
        return sum;
    }

    public static void main(String[] args) {
        long[] numbers = LongStream.rangeClosed(1, 100000000).toArray();

        Long result = new ForkJoinPool().invoke(new SumRecursiveTask(numbers));
        System.out.println("result:" +result);
    }
}

O número padrão de threads para Fork / Join é o número de seus processadores e esse valor é Runtime.getRuntime().available- Processors()derivado dele. Mas você pode java.util.concurrent.ForkJoinPool.common. parallelismalterar o tamanho do pool de threads por meio das propriedades do sistema , conforme mostrado abaixo: System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");Esta é uma configuração global, portanto, afetará todos os fluxos paralelos no código. Atualmente não há como especificar este valor especificamente para um fluxo paralelo. Porque afetará todos os fluxos paralelos, evite operações de rede / IO na tarefa, caso contrário, pode diminuir a velocidade de execução de outros fluxos paralelos


paralelismo

O que mencionamos acima é a operação de usar fluxos paralelos em Java 7. Java8 não para por aí, mas nos fornece uma maneira mais conveniente, ou seja parallelStream, a parallelStreamcamada inferior ainda é implementada por meio do framework Fork / Join.

  • Uso comum:
    1. Converter fluxo serial em fluxo paralelo
    LongStream.rangeClosed(1,1000)
                .parallel()
                .forEach(System.out::println);

2. Gere fluxos paralelos diretamente

 List<Integer> values = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            values.add(i);
        }
        values.parallelStream()
                .forEach(System.out::println);
  • Use parallelStream corretamente

Usamos parallelStreampara obter o exemplo cumulativo acima para ver o efeito, o código é o seguinte:

public static void main(String[] args) {
    Summer summer = new Summer();
    LongStream.rangeClosed(1, 100000000)
            .parallel()
            .forEach(summer::add);
    System.out.println("result:" + summer.sum);

}

static class Summer {
    public long sum = 0;

    public void add(long value) {
        sum += value;
    }
}

Os resultados são os seguintes:

resultado

Após a execução, descobrimos que o resultado da operação estava incorreto e o resultado de cada operação era diferente.
Na verdade, essa é uma parallelStreamsituação comum de uso indevido , que parallelStreamnão é segura para thread. Nesse caso, várias threads são usadas para modificar a soma da variável compartilhada e realizar uma sum += valueoperação. Essa operação em si não é atômica, portanto, você deve evitá-la ao usar streams paralelos. Modifique as variáveis ​​compartilhadas.

Modifique o exemplo acima e use parallelStream-o corretamente para obter o código da seguinte maneira:

long result = LongStream.rangeClosed(1, 100000000)
        .parallel()
        .reduce(0, Long::sum);
System.out.println("result:" + result);

Já dissemos que o processo de operação de fork / join é: desmontar as sub-partes, calcular e mesclar os resultados; como a parallelStreamcamada inferior usa a estrutura fork / join, essas etapas também precisam ser feitas, mas a partir do código acima, podemos ver Quando Long::sumfizemos os cálculos e reducefizemos os resultados mesclados, não dividimos a tarefa, então esse processo deve parallelStreamter nos ajudado a alcançá-lo, nesse momento devemos conversar sobre isso.Spliterator

SpliteratorÉ uma nova interface adicionada pelo Java8, projetada para execução paralela de tarefas.

public interface Spliterator<T> {
    boolean tryAdvance(Consumer<? super T> action);

    Spliterator<T> trySplit();

    long estimateSize();

    int characteristics();
}

tryAdvance: percorre todos os elementos, retorna verdadeiro se ainda houver travessia, caso contrário retorna falso

trySplit: divide todos os elementos em pequenas subpartes e retorna nulo se não puderem ser divididos

estimativaSize: quantos elementos restam na divisão atual

características: Retorna a codificação do conjunto de recursos atual do Spliterator


Resumindo

  1. Para provar que o processamento paralelo é mais eficiente do que o processamento sequencial, você só pode passar nos testes e não pode contar com suposições (o exemplo cumulativo neste artigo foi executado em vários computadores muitas vezes e não prova que o uso do processamento paralelo para processar a acumulação deve ser muito mais rápido do que o serial , Então ele só pode passar em vários testes, os resultados podem ser diferentes em ambientes diferentes)
  2. A quantidade de dados é pequena e a lógica de cálculo é simples. Geralmente não é recomendado o uso de fluxos paralelos
  3. Precisa considerar o consumo de tempo de operação do fluxo
  4. Em alguns casos, você mesmo precisa implementar a lógica de divisão, para que os fluxos paralelos possam ser eficientes

Obrigado por sua paciência em ler aqui.
Claro, pode haver mais ou menos deficiências e erros no artigo.Se você tiver sugestões ou opiniões, você está convidado a comentar e trocar.
Por fim, espero que os amigos gostem, comentem e sigam Sanlian, porque essas são todas as fontes de motivação para minha partilha

Acho que você gosta

Origin blog.51cto.com/15049004/2562891
Recomendado
Clasificación