Лабораторная работа №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:
- Создается матрица 1000×1000.
- Копируется три раза для разных реализаций.
- Запускается однопоточное вычисление и измеряется время выполнения.
- Запускается версия с
ThreadPoolExecutor. - Запускается версия с
ForkJoinPool. - Результаты выводятся в консоль.
Вспомогательный метод 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
Анализ результатов
-
Однопоточный алгоритм показывает линейный рост времени выполнения с увеличением размера матрицы. Это ожидаемо, так как вычисления выполняются последовательно.
-
ThreadPoolExecutorдемонстрирует худшие результаты по сравнению с остальными методами. Это связано с накладными расходами на управление потоками и передачу задач в пул потоков. Он эффективен при большом количестве независимых задач, но в данном случае не даёт прироста. -
ForkJoinPoolпоказывает лучшую производительность среди параллельных методов. Это объясняется тем, чтоForkJoinPoolоптимизирован для рекурсивного разбиения задач и эффективного использования ресурсов CPU. -
При увеличении размера матрицы разница между
ForkJoinPoolи ThreadPoolExecutor становится более заметной, так какForkJoinPoolминимизирует накладные расходы на распределение задач.
Вывод
Использование ForkJoinPool даёт наилучший результат среди многопоточных методов в данном алгоритме. ThreadPoolExecutor не оправдывает себя из-за накладных расходов на управление потоками. Однопоточный метод показывает неплохие результаты на небольших данных, но проигрывает ForkJoinPool при увеличении объёма вычислений.