Compare commits

...

18 Commits

Author SHA1 Message Date
c6f7893725 Отчёт 2025-02-12 12:43:38 +03:00
5365327d2f Пример симуляции в App.java 2025-02-12 11:55:30 +03:00
548619a0e5 Логирование смены настроек 2025-02-12 11:53:50 +03:00
eb3fa2fbad Поменял настройки симуляции (выше частоты) 2025-02-12 11:42:53 +03:00
9e5b33d600 Постепенное охлаждение и повышение влажности 2025-02-12 11:39:02 +03:00
f26810f2f8 Тесты на deadlocks и race conditions 2025-02-12 10:57:28 +03:00
0f2f9532ef Папка tmp в .gitignore 2025-02-12 10:53:55 +03:00
7868add638 Добавил вывод реальных значений температуры и влажности 2025-02-11 17:18:18 +03:00
31dcf88642 Добавил минимальное логирование 2025-02-11 17:14:49 +03:00
50fc3ceaa7 Исправил некорректный шаг снижения влажности у вентилятора 2025-02-11 17:11:54 +03:00
dbfb3b6bb0 Логика контроллера 2025-02-11 16:30:13 +03:00
5fc07ef69f Логика изменения настроек 2025-02-11 16:29:37 +03:00
8049646a8a Тесты для вентилятора 2025-02-11 12:57:39 +03:00
0e4b212f99 Логика работы вентилятора 2025-02-11 12:57:29 +03:00
0c4644e6b7 Подключил форматирование кода 2025-02-11 12:45:37 +03:00
ee8988056a Вынес функции для сна потоков в Utils 2025-02-11 12:42:28 +03:00
aef1113061 Тесты для нагревателя 2025-02-11 12:31:21 +03:00
f03ac31ae4 Логика симуляции нагревателя 2025-02-11 11:34:10 +03:00
15 changed files with 561 additions and 73 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
target
tmp

97
lab3/report/report.txt Normal file
View File

@@ -0,0 +1,97 @@
h1. Отчёт по лабораторной работе №3 (Тищенко Артём)
h2. Задание
В данной работе необходимо написать многопоточное приложение, которое эмулирует заданную модель. Студент сам должен спроектировать потоки, которые отвечают за поведение сущностей из полученного задания. Взаимодействие потоков должно быть синхронизировано и приложение должно быть протестирование на наличие dead locks и race conditions. Приложение не должно переставать работать из-за изменения задержек и модель не должна быть полностью синхронной.
h2. Мой вариант
Обогреватель — вентилятор. Каждая комната в здании имеет управляющий терминал для наблюдения и контролирования за окружающей средой. Каждый терминал измеряет и выводит текущую температуру и влажность. В каждой комнаты кроме того установлена предпочтительная температура и влажность (пара чисел). Если текущие значения температуры или влажности вышли за пределы предпочтительных настроек более чем на 1%, тогда станция включает/выключает обогреватель или вентилятор. Должны быть следующие процессы: сенсоры, настройки, обогреватель — вентилятор, контроллер.
h2. Архитектура и компоненты реализации
Приложение состоит из нескольких основных классов:
# *App*
#* Точка входа в программу.
#* Создаёт объект _Room_, объекты _Settings_ и _Controller_, затем запускает поток с контроллером.
#* При необходимости может выступать стартовой площадкой для других потоков (например, если вы решите запускать класс _Room_ как отдельный поток).
# *Room* (реализует _Runnable_)
#* Симулирует физические процессы в комнате.
#* Хранит внутренние поля:
#** _temperature_ текущая температура в градусах Цельсия.
#** _humidity_ текущая относительная влажность (0..1).
#* Периодически изменяет температуру и влажность случайным образом.
#* Методы _adjustTemperature()_ и _adjustHumidity()_ помечены как _synchronized_, чтобы избежать гонок данных.
#* При запуске в потоке (метод _run()_) постоянно вносит небольшие случайные изменения в температуру и влажность.
# *Sensor* (реализует _Runnable_)
#* Представляет набор датчиков в комнате.
#* Периодически (с заданным интервалом) считывает актуальную _temperature_ и _humidity_ из _Room_, добавляя некоторую случайную погрешность.
#* Хранит результат измерений во внутренних полях (например, _temperature_, _humidity_).
#* Эти значения затем читает _Controller_.
# *Heater* (реализует _Runnable_)
#* Нагреватель комнаты.
#* При включённом состоянии (_isOn_ = true) повышает температуру в комнате на случайную величину.
#* Работает в собственном потоке, периодически внося изменения в _Room_ (через _adjustTemperature()_).
# *Fan* (реализует _Runnable_)
#* Вентилятор, уменьшающий влажность.
#* При включённом состоянии (_isOn_ = true) понижает влажность в комнате на случайную величину.
#* Работает в собственном потоке, периодически внося изменения в _Room_ (через _adjustHumidity()_).
# *Controller* (реализует _Runnable_)
#* Логический контроллер, который:
#** Читает измерения с _Sensor_ (температура и влажность).
#** Сравнивает их с желаемыми параметрами из _Settings_.
#** Включает/выключает _Heater_ и _Fan_, если показатели выходят за пределы допустимых значений.
#* Запускается в отдельном потоке и периодически (по заданному интервалу) проверяет состояние сенсоров и управляет нагревателем/вентилятором.
#* При завершении (прерывании) аккуратно останавливает все подчинённые потоки (_Sensor_, _Heater_, _Fan_).
# *Settings* (реализует _Runnable_)
#* Хранит желаемую температуру и влажность, заданные пользователем.
#* Периодически (в методе _run()_) может изменять эти настройки, симулируя поведение человека, корректирующего параметры.
#* Хранится и используется в _Controller_ для решения, когда включать/выключать нагреватель и вентилятор.
# *Utils*
#* Вспомогательный класс с методами:
#** _sleep(...)_ безопасная пауза (учитывает _InterruptedException_).
#** _sleepRandomTime(...)_ пауза случайной длины (диапазон задаётся параметрами).
#* Используется всеми потоками для имитации задержек, дабы избежать полного синхронного шага у всех объектов.
h2. Тестирование
Ниже приведён краткий обзор тестовых классов и методов, покрывающих основные аспекты работы приложения:
# *AppTest*
#* *testNoDeadlock()* проверяет отсутствие взаимной блокировки (deadlock), когда одновременно запущены _Controller_ и _Room_.
#* *testNoRaceCondition()* тест на отсутствие гонок (race conditions), в ходе которого периодически проверяются значения температуры и влажности в _Room_.
# *FanTests*
#* *testFanOn()* проверяет, что при включённом вентиляторе влажность в комнате уменьшается, а температура остаётся без изменений.
#* *testFanOff()* проверяет, что при выключенном вентиляторе ни температура, ни влажность не меняются.
# *HeaterTests*
#* *testHeaterOn()* убеждается, что при включённом нагревателе температура в комнате увеличивается, а влажность не меняется.
#* *testHeaterOff()* при выключенном нагревателе показатели комнаты не меняются.
# *RoomTests*
#* *testTemperatureAndHumidityChange()* запускает _Room_ в отдельном потоке и проверяет, что температура и влажность действительно меняются со временем.
# *SensorTests*
#* *testSensor()* эмулирует работу сенсора с помощью макета комнаты (_MockRoom_), сверяет реальное значение температуры и влажности с тем, что возвращает сенсор, учитывая допустимую погрешность.
h3. Результаты тестов
Ниже представлена иллюстрация (вывод в консоли) после успешного запуска _mvn test_, где видно, что все тесты проходят:
!tests-results.png!
h2. Результаты работы программы
Результаты 60 секунд симуляции представлены на скриншоте ниже.
<pre>
mvn package
java -jar .\target\lab3-1.0-SNAPSHOT.jar
</pre>
!results.png!
h2. Исходный код
*Исходный код доступен на "GitHub":https://github.com/Arity-T/java_labs/tree/main/lab3 .*

View File

@@ -1,13 +1,20 @@
package ru.spbstu.telematics.java;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
import java.util.Locale;
public class App {
public static void main(String[] args) {
Locale.setDefault(Locale.ENGLISH);
Room room = new Room();
Thread roomThread = new Thread(room);
Settings settings = new Settings(room, 28, 0.4);
Thread settingsThread = new Thread(settings);
Controller controller = new Controller(room, settings);
Thread controllerThread = new Thread(controller);
roomThread.start();
settingsThread.start();
controllerThread.start();
}
}

View File

@@ -10,11 +10,93 @@ public class Controller implements Runnable {
private Room room;
private Settings settings;
private Sensor sensor;
private Thread sensorThread;
private Heater heater;
private Thread heaterThread;
private Fan fan;
private Thread fanThread;
@Override
public void run() {
public Controller(Room room, Settings settings) {
this.room = room;
this.settings = settings;
}
sensor = new Sensor(room);
sensorThread = new Thread(sensor);
heater = new Heater(room);
heaterThread = new Thread(heater);
fan = new Fan(room);
fanThread = new Thread(fan);
}
private long updateIntervalMs = 500;
private double tolerance = 0.01;
private void log(String string) {
System.out.printf("[Controller in room %s] %s\n", room.name, string);
}
@Override
public void run() {
sensorThread.start();
heaterThread.start();
fanThread.start();
log("Started sensor, heater and fan threads");
while (!Thread.interrupted()) {
double realTemperature = room.getTemperature();
double sensorTemperature = sensor.getTemperature();
double desiredTemperature = settings.getTemperature();
if (sensorTemperature < desiredTemperature * (1 - tolerance)) {
if (!heater.isOn()) {
log(String.format(
"Turning heater ON (real - %.2fC°, sensor - %.2fC°, desired %.2fC°)",
realTemperature, sensorTemperature, desiredTemperature));
heater.turnOn();
}
} else {
if (heater.isOn()) {
log(String.format(
"Turning heater OFF (real - %.2fC°, sensor - %.2fC°, desired %.2fC°)",
realTemperature, sensorTemperature, desiredTemperature));
heater.turnOff();
}
}
double realHumidity = room.getHumidity();
double sensorHumidity = sensor.getHumidity();
double desiredHumidity = settings.getHumidity();
if (sensorHumidity > desiredHumidity * (1 + tolerance)) {
if (!fan.isOn()) {
log(String.format(
"Turning fan ON (real - %.2f%%, sensor - %.2f%%, desired %.2f%%)",
realHumidity * 100, sensorHumidity * 100, desiredHumidity * 100));
fan.turnOn();
}
} else {
if (fan.isOn()) {
log(String.format(
"Turning fan OFF (real - %.2f%%, sensor - %.2f%%, desired %.2f%%)",
realHumidity * 100, sensorHumidity * 100, desiredHumidity * 100));
fan.turnOff();
}
}
Utils.sleep(updateIntervalMs);
}
sensorThread.interrupt();
heaterThread.interrupt();
fanThread.interrupt();
try {
sensorThread.join();
heaterThread.join();
fanThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

View File

@@ -1,15 +1,43 @@
package ru.spbstu.telematics.java;
import java.util.Random;
/*
* Симулирует вентилятор, установленный в комнате. Может изменять поля комнаты,
* а именно - уменьшать влажность в ней.
*/
public class Fan implements Runnable {
Room room;
private boolean isOn;
private Room room;
@Override
public void run() {
public Fan(Room room) {
this.room = room;
}
}
private volatile boolean isOn;
public boolean isOn() {
return isOn;
}
public void turnOn() {
this.isOn = true;
}
public void turnOff() {
this.isOn = false;
}
private Random random = new Random();
private double humidityMaxStep = 0.005;
private long maxStepTimeMs = 500;
@Override
public void run() {
while (!Thread.interrupted()) {
if (isOn)
room.adjustHumidity(-random.nextDouble() * humidityMaxStep);
Utils.sleepRandomTime((long) (maxStepTimeMs * 0.5), maxStepTimeMs);
}
}
}

View File

@@ -1,15 +1,43 @@
package ru.spbstu.telematics.java;
import java.util.Random;
/*
* Симулирует нагреватель, установленный в комнате. Может изменять поля комнаты,
* а именно - увеличивать температуру в ней.
*/
public class Heater implements Runnable {
Room room;
private boolean isOn;
private Room room;
@Override
public void run() {
public Heater(Room room) {
this.room = room;
}
}
private volatile boolean isOn;
public boolean isOn() {
return isOn;
}
public void turnOn() {
this.isOn = true;
}
public void turnOff() {
this.isOn = false;
}
private Random random = new Random();
private double temperatureMaxStep = 0.25;
private long maxStepTimeMs = 500;
@Override
public void run() {
while (!Thread.interrupted()) {
if (isOn)
room.adjustTemperature(random.nextDouble() * temperatureMaxStep);
Utils.sleepRandomTime((long) (maxStepTimeMs * 0.5), maxStepTimeMs);
}
}
}

View File

@@ -6,6 +6,15 @@ import java.util.Random;
* Симулирует физические процессы, протекающие в команте.
*/
public class Room implements Runnable {
static private int roomCounter;
public final String name;
public Room() {
roomCounter++;
this.name = "#" + roomCounter;
}
// Температура измеряется в градусах цельсия
private volatile double temperature = 24.0;
@@ -13,6 +22,10 @@ public class Room implements Runnable {
return temperature;
}
public synchronized void adjustTemperature(double delta) {
this.temperature += delta;
}
// Относительная влажность в процентах
private volatile double humidity = 0.5;
@@ -20,29 +33,26 @@ public class Room implements Runnable {
return humidity;
}
public synchronized void adjustHumidity(double delta) {
this.humidity += delta;
}
// Параметры произвольного изменения температуры и влажности в комнате
private Random random = new Random();
private double temperatureMaxStep = 1;
private double humidityMaxStep = 0.05;
private long maxStepTimeMs = 3000;
private double temperatureMaxStep = 0.12; // примерно 0.5% от средних значений
private double humidityMaxStep = 0.003;
private long maxStepTimeMs = 500;
@Override
public void run() {
// Пусть температура и влажность произвольно изменяются со временем
// Пусть температура и влажность почти произвольно изменяются со временем,
// но со временем становится немного холоднее (комната остывает), а влажность
// немного растёт (потому что нужно иногда проветривать).
while (!Thread.interrupted()) {
temperature += (random.nextDouble() - 0.5) * 2 * temperatureMaxStep;
humidity += (random.nextDouble() - 0.5) * 2 * humidityMaxStep;
temperature += (random.nextDouble() - 0.6) * 2 * temperatureMaxStep;
humidity += (random.nextDouble() - 0.4) * 2 * humidityMaxStep;
try {
Thread.sleep(getStepTime());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Utils.sleepRandomTime((long) (maxStepTimeMs * 0.5), maxStepTimeMs);
}
}
private long getStepTime() {
// Спим от 0.5 * maxStepTimeMs до maxSteTimeMs миллисекунд
return (long) (random.nextDouble() * 0.5 + 0.5) * maxStepTimeMs;
}
}

View File

@@ -16,17 +16,17 @@ public class Sensor implements Runnable {
private volatile double temperature;
public double getTemperature() {
return temperature;
}
return temperature;
}
// Влажность
private volatile double humidity;
private volatile double humidity;
public double getHumidity() {
return humidity;
}
return humidity;
}
// Частота считывания значений с сенсоров
// Частота считывания значений с сенсоров
private long updateIntervalMs = 1000;
// Параметры произвольной ошибки измерений сенсоров
@@ -40,11 +40,7 @@ public class Sensor implements Runnable {
temperature = room.getTemperature() + (random.nextDouble() - 0.5) * 2 * maxTemperatureError;
humidity = room.getHumidity() + (random.nextDouble() - 0.5) * 2 * maxHumidityError;
try {
Thread.sleep(updateIntervalMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Utils.sleep(updateIntervalMs);
}
}
}

View File

@@ -1,14 +1,52 @@
package ru.spbstu.telematics.java;
import java.util.Random;
/*
* Симулирует переодическое изменение настроек пользователем.
*/
public class Settings implements Runnable {
private double temperature;
private Room room;
private double temperature;
public double getTemperature() {
return temperature;
}
private double humidity;
@Override
public void run() {
public double getHumidity() {
return humidity;
}
}
public Settings(Room room, double temperature, double humidity) {
this.room = room;
this.temperature = temperature;
this.humidity = humidity;
}
// Параметры произвольного изменения настроек температуры и влажности в комнате
private Random random = new Random();
private double temperatureMaxStep = 6;
private double humidityMaxStep = 0.10;
private long maxStepTimeMs = 30000;
private void log(String string) {
System.out.printf("[Settings in room %s] %s\n", room.name, string);
}
@Override
public void run() {
while (!Thread.interrupted()) {
Utils.sleepRandomTime((long) (maxStepTimeMs * 0.5), maxStepTimeMs);
temperature += (random.nextDouble() - 0.5) * 2 * temperatureMaxStep;
humidity += (random.nextDouble() - 0.5) * 2 * humidityMaxStep;
log(String.format(
"Changed to temperature %.2fC°, humidity %.2f%%",
temperature, humidity));
}
}
}

View File

@@ -0,0 +1,21 @@
package ru.spbstu.telematics.java;
import java.util.concurrent.ThreadLocalRandom;
public class Utils {
static public void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
static public void sleepRandomTime(long from, long to) {
try {
Thread.sleep(ThreadLocalRandom.current().nextLong(from, to));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

View File

@@ -3,9 +3,76 @@ package ru.spbstu.telematics.java;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class SampleTests {
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
public class AppTest {
/**
* Тест проверяет, что при параллельном запуске всех потоков
* не возникает взаимной блокировки (deadlock).
*/
@Test
void sampleTest() {
assertTrue(true);
void testNoDeadlock() throws InterruptedException {
Room room = new Room();
Settings settings = new Settings(room, 28.0, 0.4);
Controller controller = new Controller(room, settings);
Thread controllerThread = new Thread(controller);
Thread roomThread = new Thread(room);
controllerThread.start();
roomThread.start();
// Будем проверять наличие deadlock несколько раз с периодом 1 сек
// Общая длительность теста 10 секунд
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
long[] threadIds = threadBean.findDeadlockedThreads();
// Если возвращается не null, значит обнаружен deadlock
assertNull(threadIds, "Обнаружен deadlock.");
}
controllerThread.interrupt();
roomThread.interrupt();
controllerThread.join();
roomThread.join();
}
/**
* Тест проверяет отсутствие гонок (race conditions),
* когда значения становятся очевидно некорректными или программа "падает".
*/
@Test
void testNoRaceCondition() throws InterruptedException {
Room room = new Room();
Settings settings = new Settings(room, 25.0, 0.4);
Controller controller = new Controller(room, settings);
Thread controllerThread = new Thread(controller, "Controller-Thread");
Thread roomThread = new Thread(room, "Room-Thread");
controllerThread.start();
roomThread.start();
long startTime = System.currentTimeMillis();
long testDuration = 10000;
while (System.currentTimeMillis() - startTime < testDuration) {
double t = room.getTemperature();
double h = room.getHumidity();
assertTrue(t > -50 && t < 100, "Температура вышла за пределы допустимых значений, возможна гонка");
assertTrue(h >= -0.5 && h <= 1.5, "Влажность вышла за пределы допустимых значений, возможна гонка");
Thread.sleep(200);
}
controllerThread.interrupt();
roomThread.interrupt();
controllerThread.join();
roomThread.join();
}
}

View File

@@ -0,0 +1,57 @@
package ru.spbstu.telematics.java;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.*;
public class FanTests {
Room room;
double initialTemperature;
double initialHumidity;
Fan fan;
Thread fanThread;
@BeforeEach
public void setUp() {
room = new Room();
initialTemperature = room.getTemperature();
initialHumidity = room.getHumidity();
fan = new Fan(room);
fanThread = new Thread(fan);
}
/*
* Проверяет, что включенный вентилятор уменьшает влажность в комнате
* и при этом не изменяет температуру.
*/
@Test
public void testFanOn() throws InterruptedException {
fan.turnOn();
fanThread.start();
Thread.sleep(5000);
assertEquals(initialTemperature, room.getTemperature());
assertTrue(initialHumidity > room.getHumidity());
fanThread.interrupt();
fanThread.join();
}
/*
* Проверяет, что выключенный вентилятор не изменяет температуру и влажность
* в комнате.
*/
@Test
public void testFanOff() throws InterruptedException {
fanThread.start();
Thread.sleep(5000);
assertEquals(initialHumidity, room.getHumidity());
assertEquals(initialTemperature, room.getTemperature());
fanThread.interrupt();
fanThread.join();
}
}

View File

@@ -0,0 +1,57 @@
package ru.spbstu.telematics.java;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.*;
public class HeaterTests {
Room room;
double initialTemperature;
double initialHumidity;
Heater heater;
Thread heaterThread;
@BeforeEach
public void setUp() {
room = new Room();
initialTemperature = room.getTemperature();
initialHumidity = room.getHumidity();
heater = new Heater(room);
heaterThread = new Thread(heater);
}
/*
* Проверяет, что включенный нагреватель увеличивает температуру в комнате
* и при этом не изменяет влажность.
*/
@Test
public void testHeaterOn() throws InterruptedException {
heater.turnOn();
heaterThread.start();
Thread.sleep(5000);
assertEquals(initialHumidity, room.getHumidity());
assertTrue(initialTemperature < room.getTemperature());
heaterThread.interrupt();
heaterThread.join();
}
/*
* Проверяет, что выключенный нагреватель не изменяет температуру и влажность
* в комнате.
*/
@Test
public void testHeaterOff() throws InterruptedException {
heaterThread.start();
Thread.sleep(5000);
assertEquals(initialHumidity, room.getHumidity());
assertEquals(initialTemperature, room.getTemperature());
heaterThread.interrupt();
heaterThread.join();
}
}

View File

@@ -3,7 +3,6 @@ package ru.spbstu.telematics.java;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class RoomTests {
/*
* Проверяет, что температура и влажность изменяются со временем.

View File

@@ -3,7 +3,6 @@ package ru.spbstu.telematics.java;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class SensorTests {
/*
* Моковый класс комнаты для упрощения тестирования сенсоров.
@@ -13,11 +12,11 @@ public class SensorTests {
double humidity;
public MockRoom(double temperature, double humidity) {
this.temperature = temperature;
this.humidity = humidity;
}
this.temperature = temperature;
this.humidity = humidity;
}
@Override
@Override
public double getTemperature() {
return temperature;
}
@@ -29,7 +28,8 @@ public class SensorTests {
}
/*
* Проверяет, что сенсоры выдают реальную температуру и влажность комнаты в пределах
* Проверяет, что сенсоры выдают реальную температуру и влажность комнаты в
* пределах
* некоторой погрешности.
*/
@Test