Многопоточное суммирование массива на Java — лучшие практики, структура, допустимое использование утилитного класса, отсутствие synchronized

Рейтинг: 0Ответов: 1Опубликовано: 11.08.2025

Я реализовал утилитный класс и несколько Runnable-потоков для параллельного суммирования сегментов случайно сгенерированного массива целых чисел. Код рабочий. Основные моменты:

Размер массива и число потоков задаются аргументами командной строки.

Каждый поток суммирует свою часть массива (диапазоны равного размера, последний поток может обрабатывать остаток).

В конце выводятся общая сумма (через Arrays.stream) и сумма, полученная потоками.

Что хотелось бы выяснить:

  1. Правильна ли декомпозиция на классы и методы?

  2. Уместно ли использовать утилитный класс для всех сервисных методов?

  3. Есть ли проблемы с потокобезопасностью?

  4. Какие узкие места по производительности и памяти видите?

  5. Следует ли заменить ручное управление потоками на ThreadPool/ExecutorService или parallel streams?

  6. Что добавить для продакшн-готовности (логирование, тесты, обработка ошибок)?

  7. Сейчас еще подумал, что можно было в параллельные потоки отправить заполнение массива рандомными значениями? при очень больших значениях можно сократить слоность логарифмически, хотя по заданию это конечно не требуется

Полный текст задачи с примерами: задача

код:

package ex02;

import java.util.Arrays;

/**
 * Main entry point for multithreaded array summation program.
 * Accepts command line arguments for array size and thread count,
 * creates the array and runs summators in parallel.
 */
public class Program {
        /**
         * Application main method. Checks arguments, generates array, runs threads and
         * prints results.
         * 
         * @param args Command line arguments: --arraySize=N --threadsCount=N
         * @throws InterruptedException If thread interruption occurs during join
         */
        public static void main(String[] args) throws InterruptedException {
                if (SummatorService.isArgsChecked(args)) {
                        int arraySize = SummatorService.getArraysize(
                                        args[0].substring("--arraySize=".length()));
                        int threadsCount = SummatorService.getCountOfThreads(arraySize,
                                        args[1].substring("--threadsCount=".length()));
                        int[] array = SummatorService.fillArrayRandomValues(
                                        arraySize);
                        Summator[] summators = SummatorService.createArrayWithRunnable(
                                        threadsCount, array);

                        System.out.println("Sum: " + Arrays.stream(array).sum());

                        Thread[] threads = new Thread[threadsCount];

                        for (int i = 0; i < threads.length; i++) {
                                threads[i] = new Thread(summators[i]);
                                threads[i].start();
                        }

                        for (Thread thread : threads) {
                                thread.join();
                        }

                        System.out.println("Sum by threads: "
                                        + Arrays.stream(summators)
                                                        .mapToInt(Summator::getResult)
                                                        .sum());
                }
        }
}

package ex02;

/**
 * Runnable that sums a segment of an integer array.
 * Stores its own thread index, boundaries, and result.
 */
public class Summator implements Runnable {
    /** Reference to the source array to sum */
    private int[] arrayToSum;

    /** Start index (inclusive) of segment to sum */
    private int indexStart;

    /** End index (exclusive) of segment to sum */
    private int indexEnd;

    /** Index of thread for output purposes */
    private int threadIndex;

    /** Index of thread for output purposes */
    private int result = 0;

    /**
     * Summator constructor.
     * 
     * @param array          Source array
     * @param start          Start index (inclusive)
     * @param end            End index (exclusive)
     * @param numberOfThread Thread index (for reporting)
     */
    public Summator(int[] array, int start, int end, int numberOfThread) {
        arrayToSum = array;
        indexStart = start;
        indexEnd = end;
        threadIndex = numberOfThread;
    }

    /**
     * Runs the summation for the assigned segment.
     * Prints sum and range for this thread.
     */
    @Override
    public void run() {
        for (int i = indexStart; i < indexEnd; i++) {
            result += arrayToSum[i];
        }

        System.out.printf("Thread %d: from %d to %d sum is %d%n",
                threadIndex,
                indexStart,
                indexEnd - 1,
                result);
    }

    /**
     * Gets the resulting sum calculated by this instance.
     * 
     * @return Resulting sum
     */
    public int getResult() {
        return result;
    }
}

package ex02;

import java.util.Random;

/**
 * Utility class for array generation, input parsing,
 * thread count calculation, and constructing Summator instances.
 */
public class SummatorService {
    /** Maximum allowed size of the array */
    private static final int MAX_ARRAY_SIZE = 2_000_000;

    /** Maximum modulus value for array elements */
    private static final int MAX_VALUE_FOR_RANDOM = 1_000;

    /**
     * Utility constructor. Prevents instantiation.
     */
    private SummatorService() {
        throw new UnsupportedOperationException("Utility class");
    }

    /**
     * Checks whether input arguments are valid and match the expected format.
     * 
     * @param args Command line arguments
     * @return true if valid, false otherwise
     */
    public static boolean isArgsChecked(String[] args) {
        boolean isValid = true;

        if (args.length != 2) {
            System.out.println("Usage of program: "
                    + "java Program --arraySize=N --threadsCount=N");
            isValid = false;
        } else if (!args[0].startsWith("--arraySize=")) {
            System.out.println("First argument must be "
                    + "--arraySize=N, now it is: " + args[0]);
            isValid = false;
        } else if (!args[1].startsWith("--threadsCount")) {
            System.out.println("Second argument must be "
                    + "--threadsCount=N, now it is: " + args[1]);
            isValid = false;
        }

        return isValid;
    }

    /**
     * Parses array size string and applies boundaries if out of allowed range.
     * 
     * @param arraySizeString String representing the array size
     * @return final array size in bounds
     */
    public static int getArraysize(String arraySizeString) {
        int arraySize = Integer.parseInt(arraySizeString);

        if (arraySize < 1) {
            arraySize = 1;
        } else if (arraySize > MAX_ARRAY_SIZE) {
            arraySize = MAX_ARRAY_SIZE;
        }

        return arraySize;
    }

    /**
     * Parses thread count string, clamps to array size if exceeding it.
     * 
     * @param arraySize         Size of the array
     * @param threadCountString String representing thread count
     * @return valid thread count
     */
    public static int getCountOfThreads(int arraySize, String threadCountString) {
        int threadsCount = Integer.parseInt(threadCountString);

        if (threadsCount > arraySize) {
            threadsCount = arraySize;
        }

        return threadsCount;
    }

    /**
     * Generates an array of random integers in range [0, MAX_VALUE_FOR_RANDOM)
     * 
     * @param capacity array size
     * @return new int array
     */
    public static int[] fillArrayRandomValues(int capacity) {
        int[] array = new int[capacity];

        Random random = new Random();
        for (int i = 0; i < array.length; i++) {
            array[i] = random.nextInt(MAX_VALUE_FOR_RANDOM);
        }

        return array;
    }

    /**
     * Creates an array of Summator runnables, each responsible for its own chunk of
     * the array.
     * 
     * @param numberOfThreads Number of summator threads
     * @param srcArray        Source array for summation
     * @return array of Summator instances
     */
    public static Summator[] createArrayWithRunnable(int numberOfThreads,
            int[] srcArray) {
        Summator[] array = new Summator[numberOfThreads];
        int subArrayCapacity = srcArray.length / numberOfThreads;
        int index = 0;

        for (int i = 0; i < numberOfThreads - 1; i++) {
            array[i] = new Summator(srcArray, index, index + subArrayCapacity, i + 1);
            index += subArrayCapacity;
        }

        array[numberOfThreads - 1] = new Summator(srcArray, index,
                srcArray.length, numberOfThreads);

        return array;
    }

}

Ответы

▲ 2

Правильна ли декомпозиция на классы и методы?

Уместно ли использовать утилитный класс для всех сервисных методов?

По-моему мнению типичный оверинжиниринг, но я не видел задание. Может там оверинжиниринг требованием был.

При этом при всём, ребёнка выплеснули, когда набирали тазик с оверинжинирингом - нет метода, куда бы я мог отдать массив и получить сумму. Надо самому создавать сумматоры, самому их запускать, самому суммировать.

Есть ли проблемы с потокобезопасностью?

Есть. private int result = 0 накапливается в одном потоке и потом вычитывается в другом без синхронизации. Сейчас выдаёт правильные результаты за счёт неявной синхронизации thread.join(), но потокобезопасным я бы такой код не назвал.

Какие узкие места по производительности и памяти видите?

Никаких

Следует ли заменить ручное управление потоками на ThreadPool/ExecutorService или parallel streams?

Я бы заменил

Что добавить для продакшн-готовности (логирование, тесты, обработка ошибок)?

Что это за продакшн-готовность, в которой проблема переполнения решается за счёт ограничения размера массива и максимального значения числа? В таком случае нужны проверки и тесты на случай, как поведёт себя код, если кто-то поменяет константы таким образом, что станет возможным переполнение.

в сумматоре (исправлено после дискуссии в комментах)

result = Math.addExact(result, arrayToSum[i]);

и потом то же самое, когда суммируешь сумматоры.