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
- Para usar a estrutura
RecursiveTask
Fork / Join, você primeiro precisa criar suas próprias tarefas, herdarRecursiveTask
e 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ê podejava.util.concurrent.ForkJoinPool.common. parallelism
alterar 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 parallelStream
camada inferior ainda é implementada por meio do framework Fork / Join.
- Uso comum:
1. Converter fluxo serial em fluxo paraleloLongStream.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 parallelStream
para 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:
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 parallelStream
situação comum de uso indevido , que parallelStream
não é segura para thread. Nesse caso, várias threads são usadas para modificar a soma da variável compartilhada e realizar uma sum += value
operaçã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 parallelStream
camada inferior usa a estrutura fork / join, essas etapas também precisam ser feitas, mas a partir do código acima, podemos ver Quando Long::sum
fizemos os cálculos e reduce
fizemos os resultados mesclados, não dividimos a tarefa, então esse processo deve parallelStream
ter 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
- 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)
- A quantidade de dados é pequena e a lógica de cálculo é simples. Geralmente não é recomendado o uso de fluxos paralelos
- Precisa considerar o consumo de tempo de operação do fluxo
- 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