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

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

**Необходимо:**
1. Разработать однопоточный вариант алгоритма и замерить время его работы.
2. Разработать   параллельный   вариант   алгоритма   с   использованием ThreadPoolExecutor и замерить время его работы
3. Разработать параллельный вариант алгоритма с использованием ForkJoinPoll и замерить время его работы.
Массив генерируется до работы всех вариантов алгоритмов. Все три алгоритма обрабатывают три одинаковых массива.


## Вариант 12.
Определить среднее арифметическое элементов матрицы ниже главной
диагонали.

### Запуск лаборатороной работы:
1. Компиляция программы осуществляется с помощью команды:
> javac Main.java

2. Запуск скомпилированного класса:
> java Main

### Использованные технологии:
- Пакет `java.util.concurrent` предоставляет мощные инструменты для работы с многопоточностью и параллельными вычислениями
- `ExecutorService` и `Executors` используются для управления пулами потоков и выполнения задач
- `ThreadPoolExecutor` - в этом подходе используется пул потоков для параллельного вычисления суммы элементов матрицы. Матрица разбивается на части, и каждая часть обрабатывается отдельным потоком.
- `ForkJoinPool` — специализированный пул потоков, предназначенный для выполнения задач, которые могут быть рекурсивно разбиты на более мелкие подзадачи (fork), а затем объединены (join). 
### Как работает программа:
1. Происходит генерация массива размером = size в классе `Main.java`
```
public class Main {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        int size = 1000; // size matrix
        int[][] matrix = generateMatrix(size);
```
Метод для генерации массива:
```
private static int[][] generateMatrix(int size) {
        int[][] matrix = new int[size][size];
        Random random = new Random();
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                matrix[i][j] = random.nextInt(1000);
            }
        }
        return matrix;
    }
```
2. Создание трех копий исходного массива.
3. Происходит обработка массива с помощью однопоточного варианта алгоритма ( класс SingleThreadedAverage).
4. Класс ThreadPoolAverage обрабатывает матрицу с использованием пула потоков  создается пул потоков (задается количество потоков = 4), а каждая строка матрицы обрабатывается в отдельном потоке. 
5. Класс  ForkJoinAverage обрабатывает матрицу с использованием ForkJoinPool и рекурсивного разделения задачи: если блок большой, он разделяется на две части, и каждая часть обрабатывается рекурсивно.
6. Происходит замер времени работы всех алгоритмов в классе `Main.java`:
```
  // SingleThreaded algorithm
        long startTime = System.nanoTime();
        double singleThreadedResult = SingleThreadedAverage.calculate(matrix);
        long endTime = System.nanoTime();
        System.out.println("Result: " + singleThreadedResult);
        System.out.println("Single-threaded time: " + (endTime - startTime) / 1_000_000 + " ms");

        // ThreadPoolExecutor algorithm
        startTime = System.nanoTime();
        double threadPoolResult = ThreadPoolAverage.calculate(matrix, 4);
        endTime = System.nanoTime();
        System.out.println("ThreadPoolExecutor time: " + (endTime - startTime) / 1_000_000 + " ms");

        // ForkJoinPool algorithm
        startTime = System.nanoTime();
        double forkJoinResult = ForkJoinAverage.calculate(matrix);
        endTime = System.nanoTime();
        System.out.println("ForkJoinPool time: " + (endTime - startTime) / 1_000_000 + " ms");
```
### Тесты:
- при size = 1000:
Result: 498.8887907907908
Single-threaded time: 4 ms
ThreadPoolExecutor time: 104 ms
ForkJoinPool time: 42 ms

Result: 499.8382882882883
Single-threaded time: 1 ms
ThreadPoolExecutor time: 35 ms
ForkJoinPool time: 15 ms

- при size = 10000:
Result: 499.5273448344835
Single-threaded time: 26 ms
ThreadPoolExecutor time: 42 ms
ForkJoinPool time: 18 ms

## Вывод:
При небольших размерах матрицы лучше использовать однопоточный вариант алгоритма, так как он дает лучший показатель времени. Однако, если размер генерируемой матрицы большой, то нужно использовать ForkJoinPool. А ThreadPoolExecutor - самый медленный. 