Files
SSPR_25/shipilov_nikita_lab_1

Лабораторная работа №1

Разработка многопоточного приложения с использованием Java Concurrency согласно варианту задания.

Необходимо:

  • Разработать однопоточный вариант алгоритма и замерить время его работы.
  • Разработать параллельный вариант алгоритма с использованием ThreadPoolExecutor и замерить время его работы.
  • Разработать параллельный вариант алгоритма с использованием ForkJoinPoll и замерить время его работы.

!!!. Массив генерируется до работы всех вариантов алгоритмов. Все три алгоритма обрабатывают три одинаковых массива.

Вариант задания 2: Разделить элементы матрицы на наименьший элемент.

Как запустить лабораторную работу:

javac Main.java компилирует исходный код java Main команда запускает скомпилированный класс

Какие технологии использовали:

  • Java 17+

  • ExecutorService (ThreadPoolExecutor)

  • ForkJoinPool

  • System.currentTimeMillis() для замеров времени выполнения

Как работает программа:

Вот описание кода с пояснениями:


Описание программы


1. Генерация матрицы

public static int[][] generateMatrix(int rows, int cols) {
    int[][] matrix = new int[rows][cols];
    Random random = new Random();
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = random.nextInt(100) + 1;
        }
    }
    return matrix;
}

Метод generateMatrix создает двумерную матрицу размером rows × cols, заполняя её случайными числами от 1 до 100.


2. Поиск минимального элемента

public static int findMin(int[][] matrix) {
    int min = Integer.MAX_VALUE;
    for (int[] row : matrix) {
        for (int value : row) {
            if (value < min) {
                min = value;
            }
        }
    }
    return min;
}

Метод findMin находит минимальный элемент в матрице, который затем будет использоваться для деления элементов.


3. Однопоточное деление

public static void singleThreadedDivision(int[][] matrix) {
    int min = findMin(matrix);
    for (int i = 0; i < matrix.length; i++) {
        for (int j = 0; j < matrix[i].length; j++) {
            matrix[i][j] /= min;
        }
    }
}

В этом методе вся работа выполняется в одном потоке перебираются элементы матрицы и делятся на минимальное значение.


4. Деление с использованием ThreadPoolExecutor

public static void threadPoolDivision(int[][] matrix, int numThreads) {
    int min = findMin(matrix);
    ExecutorService executor = Executors.newFixedThreadPool(numThreads);
    for (int i = 0; i < matrix.length; i++) {
        final int row = i;
        executor.submit(() -> {
            for (int j = 0; j < matrix[row].length; j++) {
                matrix[row][j] /= min;
            }
        });
    }
    executor.shutdown();
    try {
        executor.awaitTermination(1, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

Метод threadPoolDivision использует ThreadPoolExecutor с фиксированным числом потоков (numThreads). Каждая строка матрицы обрабатывается отдельным потоком.

Преимущества:

  • Простота использования.
  • Хорошо подходит для обработки строк матрицы в параллельном режиме.

Недостатки:

  • Ограничен количеством потоков в пуле.
  • Может быть менее эффективным, чем ForkJoinPool для больших данных.

5. Деление с использованием ForkJoinPool

public static void forkJoinDivision(int[][] matrix) {
    int min = findMin(matrix);
    ForkJoinPool pool = new ForkJoinPool();
    pool.invoke(new DivideTask(matrix, min, 0, matrix.length));
}

Метод forkJoinDivision использует ForkJoinPool, который автоматически управляет задачами и распределяет их между потоками.

Вспомогательный класс DivideTask

private static class DivideTask extends RecursiveAction {
    private final int[][] matrix;
    private final int min;
    private final int start;
    private final int end;

    DivideTask(int[][] matrix, int min, int start, int end) {
        this.matrix = matrix;
        this.min = min;
        this.start = start;
        this.end = end;
    }

    @Override
    protected void compute() {
        if (end - start <= 10) {
            for (int i = start; i < end; i++) {
                for (int j = 0; j < matrix[i].length; j++) {
                    matrix[i][j] /= min;
                }
            }
        } else {
            int mid = (start + end) / 2;
            invokeAll(new DivideTask(matrix, min, start, mid),
                      new DivideTask(matrix, min, mid, end));
        }
    }
}

Как работает ForkJoinPool?

  • Разбивает работу на части (рекурсивное разбиение).
  • Запускает задачи параллельно.
  • Объединяет результаты.

Преимущества:

  • Автоматически балансирует нагрузку.
  • Эффективнее, чем ThreadPoolExecutor, при большом количестве данных.

6. Основной метод main

public static void main(String[] args) {
    int rows = 1000;
    int cols = 1000;
    int[][] matrix = generateMatrix(rows, cols);

    int[][] singleThreadedMatrix = copyMatrix(matrix);
    long startTime = System.currentTimeMillis();
    singleThreadedDivision(singleThreadedMatrix);
    long endTime = System.currentTimeMillis();
    System.out.println("Single thread algorithm: " + (endTime - startTime) + " ms");

    int[][] threadPoolMatrix = copyMatrix(matrix);
    startTime = System.currentTimeMillis();
    threadPoolDivision(threadPoolMatrix, 4);
    endTime = System.currentTimeMillis();
    System.out.println("ThreadPoolExecutor: " + (endTime - startTime) + " ms");

    int[][] forkJoinMatrix = copyMatrix(matrix);
    startTime = System.currentTimeMillis();
    forkJoinDivision(forkJoinMatrix);
    endTime = System.currentTimeMillis();
    System.out.println("ForkJoinPool: " + (endTime - startTime) + " ms");
}

В main:

  1. Создается матрица 1000×1000.
  2. Копируется три раза для разных реализаций.
  3. Запускается однопоточное вычисление и измеряется время выполнения.
  4. Запускается версия с ThreadPoolExecutor.
  5. Запускается версия с ForkJoinPool.
  6. Результаты выводятся в консоль.

Вспомогательный метод copyMatrix

public static int[][] copyMatrix(int[][] matrix) {
    int[][] copy = new int[matrix.length][matrix[0].length];
    for (int i = 0; i < matrix.length; i++) {
        System.arraycopy(matrix[i], 0, copy[i], 0, matrix[i].length);
    }
    return copy;
}

Этот метод делает копию матрицы перед запуском каждого алгоритма, чтобы тестирование было честным.

(Размер матрицы 1000х1000)

Single thread alghoritm: 20 ms
ThreadPoolExecutor: 73 ms
ForkJoinPool: 28 ms

(Размер матрицы 2000х2000)

Single thread alghoritm: 39 ms
ThreadPoolExecutor: 110 ms
ForkJoinPool: 51 ms.

(Размер матрицы 3000x3000)

Single thread alghoritm: 69 ms
ThreadPoolExecutor: 182 ms
ForkJoinPool: 106 ms

(Размер матрицы 4000x4000)

Single thread alghoritm: 108 ms
ThreadPoolExecutor: 237 ms
ForkJoinPool: 134 ms

Анализ результатов

  1. Однопоточный алгоритм показывает линейный рост времени выполнения с увеличением размера матрицы. Это ожидаемо, так как вычисления выполняются последовательно.

  2. ThreadPoolExecutor демонстрирует худшие результаты по сравнению с остальными методами. Это связано с накладными расходами на управление потоками и передачу задач в пул потоков. Он эффективен при большом количестве независимых задач, но в данном случае не даёт прироста.

  3. ForkJoinPool показывает лучшую производительность среди параллельных методов. Это объясняется тем, что ForkJoinPool оптимизирован для рекурсивного разбиения задач и эффективного использования ресурсов CPU.

  4. При увеличении размера матрицы разница между ForkJoinPool и ThreadPoolExecutor становится более заметной, так как ForkJoinPool минимизирует накладные расходы на распределение задач.

Вывод

Использование ForkJoinPool даёт наилучший результат среди многопоточных методов в данном алгоритме. ThreadPoolExecutor не оправдывает себя из-за накладных расходов на управление потоками. Однопоточный метод показывает неплохие результаты на небольших данных, но проигрывает ForkJoinPool при увеличении объёма вычислений.