[Javaマルチスレッド] Javaでの並列処理タスクの開発について

I.はじめに

並列多个线程一起运行,来提高系统的整体处理速度すなわち:

  • 複数のスレッドを使用すると処理速度が向上するのはなぜですか。現在、コンピューターは一般的多核处理器に使用されているため、充電する必要があり分利用cpu资源ます。ステーションが少し高い場合は每台机器都可以是一个处理节点多台机器并行处理並列処理はどこにでもあると言えます。

この記事では、主にJavaが並列処理を実装する方法について説明します。

2.どこでも並列

Javaのガベージコレクタ、我々はから、GC短い遅延に伴い、更新のすべての世代のバージョンを見ることができます今して、Javaは帽子を脱いでゆっくりしてきました。初期のメッセージキューから現在および概念の導入まで、メッセージの並列処理を改善します。データベース内の1つのテーブルのデータが一定のレベルに達した後、アクセス速度が非常に遅くなります私たちがします。処理し、インポートするテーブルを、あなたはRedisの自体の処理はシングルスレッドであることを考えるかもしれないが、コンセプトで導入Redisクラスターソリューション;より一般的なのは、通常、複数のユニットをデプロイそれらを介して配布する多くのビジネスシステムです。他にもいくつかの例がありますが、ここではすべてをリストしません。serialcmsG1ActiveMQkafkaRocketMQ分区分表数据库中间件slot(槽)负载均衡中间件

Javaガベージコレクター-シリアル、パラレル、CMS、G1コレクターの概要
JVMガベージコレクター-シリアル、パラレル、CMS、G1の比較

3.並列化する方法

私は、並列だと思うのコアという"拆分"把大任务变成小任务,然后利用多核CPU也好,还是多节点也好,同时并行的处理Javaのアップデートの古代のバージョンは、最初から、私たちの開発者にとってより便利に提供するために、並列に処理されるThreadと、线程池に、fork/join框架最終的に、契約

簡単な合計の例を使用して、さまざまなメソッドがどのように並行して処理されるかを見てみましょう。

3.1。シングルスレッド処理

最初に最も単純なシングルスレッド処理方法を見て、合計操作にメインスレッドを直接使用します。

public class SingleThread {
    
    
    public static void main(String[] args) {
    
    
    	//生成指定范围大小的的数组
        long[] numbers = LongStream.rangeClosed(1, 10_000_000).toArray();
        long sum = 0;
        for (int i = 0; i < numbers.length; i++) {
    
    
            sum += numbers[i];
        }
        System.out.println("sum  = " + sum);
    }
}

合計自体は、計算集約的なタスクが、今多核时代、唯一の单线程、唯一の使用に相当します一个cpu其他cpu被闲置,导致资源的浪费

3.2。スレッドメソッド

入れ任务拆分成多个小任务,然后每个小任务分别启动一个线程,分段处理任务ます。次のように:

public class ThreadTest {
    
    
    //分段阈值,即每个线程处理次数
    public static final int threshold = 10_000;
    //要累加的数字集合
    public static long[] numbers;
    //累加结果
    private static long allSum;

    public static void main(String[] args) throws Exception {
    
    
        //生成要累加的数字集合
        numbers = LongStream.rangeClosed(1, 10_000_000).toArray();

        //线程数 =计算总次数 / 每个线程处理次数
        int taskSize = (int) (numbers.length / threshold);

        //循环生成线程
        for (int i = 1; i <= taskSize; i++) {
    
    
            final int key = i;
            new Thread(new Runnable() {
    
    
                public void run() {
    
    
                    //一个线程处理数组的一段数据  start= (i - * threshold) ,end = key * threshold,类似于分页计算公式
                    sumAll(segmentSum((key - 1) * threshold, key * threshold));
                }
            }).start();
        }

        Thread.sleep(100);
        System.out.println("allSum = " + getAllSum());
    }

    //累加每个线程计算的总和
    private static synchronized long sumAll(long threadSum) {
    
    
        return allSum += threadSum;
    }

    //获取总和
    public static synchronized long getAllSum() {
    
    
        return allSum;
    }

    /**
     * 分段累加
     * @param start 开始下标
     * @param end   结束下标
     * @return
     */
    private static long segmentSum(int start, int end) {
    
    
        long sum = 0;
        for (int i = start; i < end; i++) {
    
    
            sum += numbers[i];
        }
        return sum;
    }
}

上記のセクションでは、大きなタスクが小さなタスクに分割されています。次に、セグメンテーションのしきい値を介して、生成されるスレッドの数と各セグメントで処理されるタスクの数が計算されます。この治療法は创建的线程数过多,而CPU数有限、さらに重要なことに求和是一个计算密集型任务启动过多的线程只会带来更多的线程上下文切换です。同時に线程处理完一个任务就终止了,也是对资源的浪费さらに、メインスレッドはサブタスクがいつ処理されたかを認識しておらず、追加の処理が必要であることがわかります。そのため、Javaはその後スレッドプールを導入しました

3.3。スレッドプールモード

并发包java.concurrentスレッドプールを含むJava1.5導入されたThreadPoolExecutor、関連するコードは次のとおりです。

public class ExecutorServiceTest {
    
    
    //分段阈值,即每个线程处理次数
    public static final int threshold = 10_000;
    //要累加的数字集合(即)
    public static long[] numbers;

    public static void main(String[] args) throws Exception {
    
    
        //生成要累加的数字集合
        numbers = LongStream.rangeClosed(1, 10_000_000).toArray();

        //创建固定长度的线程池,核心线程数大于与非核心线程大小相等=cpu核心数+1
        ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1);

        //CompletionService实际上可以看做是Executor和BlockingQueue的结合体。CompletionService在接收到要执行的任务时,通过类似BlockingQueue的put和take获得任务执行的结果。CompletionService的一个实现是ExecutorCompletionService,
        CompletionService<Long> completionService = new ExecutorCompletionService<Long>(executor);

        //线程数 =计算总次数 / 每个线程处理次数
        int taskSize = numbers.length / threshold;

        //循环生成线程
        for (int i = 1; i <= taskSize; i++) {
    
    
            final int key = i;
            completionService.submit(new Callable<Long>() {
    
    
                @Override
                public Long call() throws Exception {
    
    
                    //一个线程处理数组的一段数据  start= (i - * threshold) ,end = key * threshold,类似于分页计算公式
                    return segmentSum((key - 1) * threshold, key * threshold);
                }
            });
        }

        long sumValue = 0;
        for (int i = 0; i < taskSize; i++) {
    
    
            //检索并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则等待。
            sumValue += completionService.take().get();
        }

        // 所有任务已经完成,关闭线程池
        System.out.println("sumValue = " + sumValue);
        executor.shutdown();
    }

    /**
     * 分段累加
     * @param start 开始下标
     * @param end   结束下标
     * @return
     */
    private static long segmentSum(int start, int end) {
    
    
        long sum = 0;
        for (int i = start; i < end; i++) {
    
    
            sum += numbers[i];
        }
        return sum;
    }
}

计算密集型ビジネスは上記分析并不是线程越多越好され、ここで作成されますJDK默认的线程数:CPU数+1。これは、多くのテストの後に得られた結果です。名前が示すように、スレッドプールは次のようになります。可以重复利用现有的线程

  • 活かしながらCompletionService对子任务进行汇总
  • スレッドプールの合理的な使用は、すでに完全に並列処理タスクを実行できますが、書き込みは少し面倒です。現時点でfork/join框架Java1.7が導入されています。

3.4。fork/ joinフレームワーク

ブランチ/マージャーフレームワークの目的は以递归的方式将可以并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果次のとおりです。;関連するコードは次のとおりです。

public class ForkJoinTest extends RecursiveTask<Long> {
    
    
    //分段阈值,即每个线程处理次数
    public static final int threshold = 10_000;
    //要累加的数字集合(即)
    private final long[] numbers;
    //当前任务集合开始下标
    private final int start;
    //当前任务集合结束下标
    private final int end;

    //构造方法(初始化要累加的数字集合,开始下标,结束下标)
    private ForkJoinTest(long[] numbers, int start, int end) {
    
    
        this.numbers = numbers;
        this.start = start;
        this.end = end;
    }

    public static void main(String[] args) {
    
    
        //要累加的数字集合(即)
        long[] numbers = LongStream.rangeClosed(1, 10_000_000).toArray();


        // 创建包含Runtime.getRuntime().availableProcessors()返回值作为个数的并行线程的ForkJoinPool
        ForkJoinPool forkJoinPool = new ForkJoinPool();

        // 提交可分解的PrintTask任务
        //Future<Long> future = forkJoinPool.submit(new ForkJoinTest(numbers, 0, numbers.length));
        //System.out.println("计算出来的总和="+future.get());

        //创建ForkJoin 任务
        ForkJoinTask<Long> task = new ForkJoinTest(numbers,0, numbers.length);
        Long sumAll = forkJoinPool.invoke(task);
        System.out.println("计算出来的总和=" + sumAll);

        // 关闭线程池
        forkJoinPool.shutdown();
    }

    @Override
    protected Long compute() {
    
    
        //总处理次数
        int length = end - start;
        // 当end-start的值小于threshold时候,直接累加
        if (length <= threshold) {
    
    
            long sum = 0;
            for (int i = start; i < end; i++) {
    
    
                sum += numbers[i];
            }
            return sum;
        }
        
        System.err.println("=====任务分解======");
        // 将大任务从中间切分,然后分解成两个小任务
        int middle = (start + end) / 2;

        //任务分解: 将大任务分解成两个小任务
        ForkJoinTest leftTask = new ForkJoinTest(numbers, start, middle);
        ForkJoinTest rightTask = new ForkJoinTest(numbers, middle, end);

        // 并行执行两个小任务
        leftTask.fork();
        rightTask.fork();

        // 注:join方法会阻塞,因此有必要在两个子任务的计算都开始之后才执行join方法
        // 把两个小任务累加的结果合并起来
        return leftTask.join() + rightTask.join();
    }
}

結果:
ここに画像の説明を挿入

ForkJoinPoolこれはExecutorServiceインターフェースの実装ですサブタスクはスレッドプール内のワーカースレッドに割り当てられます。同時に、タスクはこのスレッドプールに送信される必要があります。RecursiveTask<R>サブクラスを作成する必要があります

  • 一般的なロジックは通过fork(0进行拆分次のとおりです通过join()进行结果的合并。Javaはフレームワークを提供します。入力するだけでよいので、より便利です。
  • 分割を保存し、でJava1.8導入された概念を自動的に分割してマージする簡単な方法はありますか?

3.5。パラレルストリームモード

Java 8では、並列処理をより有効に活用できるストリームの概念が導入されました。ストリームを使用するためのコードは次のとおりです。

public class StreamTest {
    
    
    public static void main(String[] args) {
    
    
        // 并行流:多个线程同时运行
        System.out.println("sum = " + parallelRangedSum(10_000_000));
        // 顺序流:使用主线程,单线程
        System.out.println("sum = " + sequentialRangedSum(10_000_000));
    }
    
    //并行流
    public static long parallelRangedSum(long n) {
    
    
        return LongStream.rangeClosed(1, n).parallel().reduce(0L, Long::sum);
    }
    //顺序流
    public static long sequentialRangedSum(long n) {
    
    
        return LongStream.rangeClosed(1, n).sequential().reduce(0L, Long::sum);
    }
}

上記のコードは非常に単純ですか?開発者にとっては完全不需要手动拆分,使用同步机制等方式であり、タスクは並行して処理できますストリームのparallel()メソッドを使用するだけで済みます系统自动会对任务进行拆分。もちろん、共有の可変状態がないことが前提です。

  • 並列ストリームの内部使用は、フォーク/結合フレームワークでもあります

結論
この記事では、合計の例を使用して並列処理を紹介します。Javaは、より便利な並列処理を提供するために懸命に取り組んでいることがわかります。

おすすめ

転載: blog.csdn.net/qq877728715/article/details/113987179