From 1041a12914b934549ad4f4c8a6ee40dbb9652454 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Sun, 14 Sep 2025 11:54:15 +0000 Subject: [PATCH 1/7] add deadline --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d7a6ba3..e974d43 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/qcWcnElX) # Java concurrency # Цели и задачи л/р: From 669f698b73984a678f7e277a0b6b05edb0600828 Mon Sep 17 00:00:00 2001 From: poma12390 Date: Tue, 16 Sep 2025 21:56:27 +0300 Subject: [PATCH 2/7] create classes --- .gitignore | 1 + src/main/java/org/labs/Main.java | 9 ++- src/main/java/org/labs/app/Simulation.java | 4 + .../org/labs/config/SimulationConfig.java | 76 +++++++++++++++++++ src/main/java/org/labs/core/DiningTable.java | 71 +++++++++++++++++ .../java/org/labs/core/actors/Programmer.java | 76 +++++++++++++++++++ .../org/labs/core/resources/LockingSpoon.java | 30 ++++++++ .../java/org/labs/core/resources/Spoon.java | 21 +++++ .../org/labs/core/stock/AtomicFoodStock.java | 17 +++++ .../java/org/labs/core/stock/FoodStock.java | 14 ++++ .../org/labs/metrics/SimulationStats.java | 25 ++++++ .../java/org/labs/model/ProgrammerState.java | 10 +++ .../java/org/labs/model/RefillRequest.java | 14 ++++ .../deadlock/DeadlockAvoidanceStrategy.java | 9 +++ .../deadlock/NMinusOneDeadlockStrategy.java | 17 +++++ .../OrderedSpoonsDeadlockStrategy.java | 13 ++++ .../strategy/fairness/EqualShareFairness.java | 25 ++++++ .../strategy/fairness/FairnessStrategy.java | 17 +++++ .../java/org/labs/waiter/DefaultWaiter.java | 40 ++++++++++ src/main/java/org/labs/waiter/Waiter.java | 14 ++++ src/test/java/SimulationTest.java | 2 + src/test/java/core/FoodStockTest.java | 4 + src/test/java/core/SpoonTest.java | 4 + .../java/strategy/DeadlockStrategiesTest.java | 4 + .../java/strategy/FairnessStrategiesTest.java | 4 + 25 files changed, 520 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/labs/app/Simulation.java create mode 100644 src/main/java/org/labs/config/SimulationConfig.java create mode 100644 src/main/java/org/labs/core/DiningTable.java create mode 100644 src/main/java/org/labs/core/actors/Programmer.java create mode 100644 src/main/java/org/labs/core/resources/LockingSpoon.java create mode 100644 src/main/java/org/labs/core/resources/Spoon.java create mode 100644 src/main/java/org/labs/core/stock/AtomicFoodStock.java create mode 100644 src/main/java/org/labs/core/stock/FoodStock.java create mode 100644 src/main/java/org/labs/metrics/SimulationStats.java create mode 100644 src/main/java/org/labs/model/ProgrammerState.java create mode 100644 src/main/java/org/labs/model/RefillRequest.java create mode 100644 src/main/java/org/labs/strategy/deadlock/DeadlockAvoidanceStrategy.java create mode 100644 src/main/java/org/labs/strategy/deadlock/NMinusOneDeadlockStrategy.java create mode 100644 src/main/java/org/labs/strategy/deadlock/OrderedSpoonsDeadlockStrategy.java create mode 100644 src/main/java/org/labs/strategy/fairness/EqualShareFairness.java create mode 100644 src/main/java/org/labs/strategy/fairness/FairnessStrategy.java create mode 100644 src/main/java/org/labs/waiter/DefaultWaiter.java create mode 100644 src/main/java/org/labs/waiter/Waiter.java create mode 100644 src/test/java/SimulationTest.java create mode 100644 src/test/java/core/FoodStockTest.java create mode 100644 src/test/java/core/SpoonTest.java create mode 100644 src/test/java/strategy/DeadlockStrategiesTest.java create mode 100644 src/test/java/strategy/FairnessStrategiesTest.java diff --git a/.gitignore b/.gitignore index b63da45..8ec7327 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/test/**/build/ ### IntelliJ IDEA ### +.idea .idea/modules.xml .idea/jarRepositories.xml .idea/compiler.xml diff --git a/src/main/java/org/labs/Main.java b/src/main/java/org/labs/Main.java index 9917247..534e12f 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -2,6 +2,13 @@ public class Main { public static void main(String[] args) { - System.out.println("Hello, World!"); + // TODO: + // 1) собрать SimulationConfig из args/дефолтов + // 2) создать ложки (count = programmers) + // 3) создать shared BlockingQueue, FoodStock + // 4) выбрать стратегии DeadlockAvoidanceStrategy и FairnessStrategy + // 5) собрать официантов и программистов + // 6) собрать DiningTable и вызвать start()/awaitCompletion() + System.out.println("hello"); } } \ No newline at end of file diff --git a/src/main/java/org/labs/app/Simulation.java b/src/main/java/org/labs/app/Simulation.java new file mode 100644 index 0000000..67f2dbc --- /dev/null +++ b/src/main/java/org/labs/app/Simulation.java @@ -0,0 +1,4 @@ +package org.labs.app; + +public class Simulation { +} diff --git a/src/main/java/org/labs/config/SimulationConfig.java b/src/main/java/org/labs/config/SimulationConfig.java new file mode 100644 index 0000000..7177d98 --- /dev/null +++ b/src/main/java/org/labs/config/SimulationConfig.java @@ -0,0 +1,76 @@ +package org.labs.config; + +import java.time.Duration; + +public final class SimulationConfig { + private final int programmers; + private final int waiters; + private final long totalPortions; + private final Duration minThink; + private final Duration maxThink; + private final Duration minEat; + private final Duration maxEat; + private final Duration spoonAcquireTimeout; + private final boolean fairShareRequired; + + private SimulationConfig( + int programmers, + int waiters, + long totalPortions, + Duration minThink, + Duration maxThink, + Duration minEat, + Duration maxEat, + Duration spoonAcquireTimeout, + boolean fairShareRequired + ) { + this.programmers = programmers; + this.waiters = waiters; + this.totalPortions = totalPortions; + this.minThink = minThink; + this.maxThink = maxThink; + this.minEat = minEat; + this.maxEat = maxEat; + this.spoonAcquireTimeout = spoonAcquireTimeout; + this.fairShareRequired = fairShareRequired; + } + + public int programmers() { return programmers; } + public int waiters() { return waiters; } + public long totalPortions() { return totalPortions; } + public Duration minThink() { return minThink; } + public Duration maxThink() { return maxThink; } + public Duration minEat() { return minEat; } + public Duration maxEat() { return maxEat; } + public Duration spoonAcquireTimeout() { return spoonAcquireTimeout; } + public boolean fairShareRequired() { return fairShareRequired; } + + public static Builder builder() { return new Builder(); } + + public static final class Builder { + private int programmers = 7; + private int waiters = 2; + private long totalPortions = 1_000_000L; + private Duration minThink = Duration.ofMillis(50); + private Duration maxThink = Duration.ofMillis(200); + private Duration minEat = Duration.ofMillis(50); + private Duration maxEat = Duration.ofMillis(200); + private Duration spoonAcquireTimeout = Duration.ofMillis(250); + private boolean fairShareRequired = true; + + public Builder programmers(int v) { this.programmers = v; return this; } + public Builder waiters(int v) { this.waiters = v; return this; } + public Builder totalPortions(long v) { this.totalPortions = v; return this; } + public Builder thinkRange(Duration min, Duration max) { this.minThink = min; this.maxThink = max; return this; } + public Builder eatRange(Duration min, Duration max) { this.minEat = min; this.maxEat = max; return this; } + public Builder spoonAcquireTimeout(Duration v) { this.spoonAcquireTimeout = v; return this; } + public Builder fairShareRequired(boolean v) { this.fairShareRequired = v; return this; } + + public SimulationConfig build() { + // validate inputs (N>1, waiters>0, etc.) + return new SimulationConfig( + programmers, waiters, totalPortions, minThink, maxThink, minEat, maxEat, spoonAcquireTimeout, fairShareRequired + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/labs/core/DiningTable.java b/src/main/java/org/labs/core/DiningTable.java new file mode 100644 index 0000000..0fe4f1c --- /dev/null +++ b/src/main/java/org/labs/core/DiningTable.java @@ -0,0 +1,71 @@ +package org.labs.core; + +import org.labs.config.SimulationConfig; +import org.labs.core.actors.Programmer; +import org.labs.core.resources.Spoon; +import org.labs.core.stock.FoodStock; +import org.labs.metrics.SimulationStats; +import org.labs.model.RefillRequest; +import org.labs.strategy.deadlock.DeadlockAvoidanceStrategy; +import org.labs.strategy.fairness.FairnessStrategy; +import org.labs.waiter.Waiter; + +import java.util.List; +import java.util.concurrent.BlockingQueue; + +public final class DiningTable { + private final SimulationConfig cfg; + private final List spoons; + private final List programmers; + private final List waiterThreads; + private final List waiters; + private final BlockingQueue refillQueue; + private final FoodStock stock; + private final DeadlockAvoidanceStrategy deadlockStrategy; + private final FairnessStrategy fairness; + private final SimulationStats stats; + + public DiningTable( + SimulationConfig cfg, + List spoons, + List programmers, + List waiters, + List waiterThreads, + BlockingQueue refillQueue, + FoodStock stock, + DeadlockAvoidanceStrategy deadlockStrategy, + FairnessStrategy fairness, + SimulationStats stats + ) { + this.cfg = cfg; + this.spoons = spoons; + this.programmers = programmers; + this.waiters = waiters; + this.waiterThreads = waiterThreads; + this.refillQueue = refillQueue; + this.stock = stock; + this.deadlockStrategy = deadlockStrategy; + this.fairness = fairness; + this.stats = stats; + } + + /** Запуск всех потоков. */ + public void start() { + // TODO: старт официантов и программистов + throw new UnsupportedOperationException("Not implemented"); + } + + /** Дождаться завершения (нет еды или достигнуты критерии остановки). */ + public void awaitCompletion() throws InterruptedException { + // TODO: join потоков + throw new UnsupportedOperationException("Not implemented"); + } + + /** Аварийная остановка. */ + public void shutdownNow() { + // TODO: прервать все потоки, закрыть ресурсы + throw new UnsupportedOperationException("Not implemented"); + } + + public SimulationStats stats() { return stats; } +} \ No newline at end of file diff --git a/src/main/java/org/labs/core/actors/Programmer.java b/src/main/java/org/labs/core/actors/Programmer.java new file mode 100644 index 0000000..6478f57 --- /dev/null +++ b/src/main/java/org/labs/core/actors/Programmer.java @@ -0,0 +1,76 @@ +package org.labs.core.actors; + +import org.labs.config.SimulationConfig; +import org.labs.core.resources.Spoon; +import org.labs.model.ProgrammerState; +import org.labs.model.RefillRequest; +import org.labs.strategy.deadlock.DeadlockAvoidanceStrategy; +import org.labs.strategy.fairness.FairnessStrategy; + +import java.util.concurrent.BlockingQueue; + +public final class Programmer implements Runnable { + private final int id; + private final Spoon left; + private final Spoon right; + private final SimulationConfig cfg; + private final DeadlockAvoidanceStrategy deadlockStrategy; + private final FairnessStrategy fairness; + private final BlockingQueue refillQueue; + private volatile ProgrammerState state = ProgrammerState.THINKING; + private long portionsEaten; + + public Programmer( + int id, + Spoon left, + Spoon right, + SimulationConfig cfg, + DeadlockAvoidanceStrategy deadlockStrategy, + FairnessStrategy fairness, + BlockingQueue refillQueue + ) { + this.id = id; + this.left = left; + this.right = right; + this.cfg = cfg; + this.deadlockStrategy = deadlockStrategy; + this.fairness = fairness; + this.refillQueue = refillQueue; + } + + public int id() { return id; } + public ProgrammerState state() { return state; } + public long portionsEaten() { return portionsEaten; } + + @Override + public void run() { + // TODO: цикл пока есть еда в системе; + // - think() + // - deadlockStrategy.beforeAcquire(id) + // - попытка взять две ложки (учитывая таймаут из cfg) + // - fairness.tryEnterEat(...) + // - eat() + // - освободить ложки + deadlockStrategy.afterRelease(id) + // - если суп кончился в тарелке – requestRefill() + // Логи/метрики по состояниям. + throw new UnsupportedOperationException("Not implemented"); + } + + /** Подумать/поболтать. */ + void think() { + // TODO: sleep в диапазоне cfg.minThink..cfg.maxThink + throw new UnsupportedOperationException("Not implemented"); + } + + /** Пожевать супчик (пока у программиста есть порция в тарелке). */ + void eat() { + // TODO: sleep в диапазоне cfg.minEat..cfg.maxEat; увеличить portionsEaten + throw new UnsupportedOperationException("Not implemented"); + } + + /** Попросить долив у официанта. */ + void requestRefill() { + // TODO: добавить RefillRequest в refillQueue + throw new UnsupportedOperationException("Not implemented"); + } +} \ No newline at end of file diff --git a/src/main/java/org/labs/core/resources/LockingSpoon.java b/src/main/java/org/labs/core/resources/LockingSpoon.java new file mode 100644 index 0000000..6d9b201 --- /dev/null +++ b/src/main/java/org/labs/core/resources/LockingSpoon.java @@ -0,0 +1,30 @@ +package org.labs.core.resources; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +final class LockingSpoon implements Spoon { + private final int id; + + public LockingSpoon(int id) { + this.id = id; + } + + @Override public int id() { return id; } + + @Override + public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void release() { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Optional underlyingLock() { + return Optional.empty(); + } +} \ No newline at end of file diff --git a/src/main/java/org/labs/core/resources/Spoon.java b/src/main/java/org/labs/core/resources/Spoon.java new file mode 100644 index 0000000..41b09ae --- /dev/null +++ b/src/main/java/org/labs/core/resources/Spoon.java @@ -0,0 +1,21 @@ +package org.labs.core.resources; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +public interface Spoon { + int id(); + + /** + * Попытаться захватить ложку в течение таймаута. + * @return true если захвачена, иначе false + */ + boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException; + + /** Освободить ложку. */ + void release(); + + /** Для метрик/отладки – предоставьте доступ к базовой блокировке при необходимости. */ + Optional underlyingLock(); +} \ No newline at end of file diff --git a/src/main/java/org/labs/core/stock/AtomicFoodStock.java b/src/main/java/org/labs/core/stock/AtomicFoodStock.java new file mode 100644 index 0000000..36baefa --- /dev/null +++ b/src/main/java/org/labs/core/stock/AtomicFoodStock.java @@ -0,0 +1,17 @@ +package org.labs.core.stock; + +final class AtomicFoodStock implements FoodStock { + public AtomicFoodStock(long initialPortions) { + // TODO: init atomic counter + } + + @Override + public boolean tryTakeOne() { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public long remaining() { + throw new UnsupportedOperationException("Not implemented"); + } +} \ No newline at end of file diff --git a/src/main/java/org/labs/core/stock/FoodStock.java b/src/main/java/org/labs/core/stock/FoodStock.java new file mode 100644 index 0000000..b8546fa --- /dev/null +++ b/src/main/java/org/labs/core/stock/FoodStock.java @@ -0,0 +1,14 @@ +package org.labs.core.stock; + +public interface FoodStock { + /** + * Попытаться выдать 1 порцию. Возвращает true, если удалось уменьшить запас. + */ + boolean tryTakeOne(); + + /** Сколько порций осталось. */ + long remaining(); + + /** Признак завершения – запаса больше нет. */ + default boolean isDepleted() { return remaining() <= 0; } +} \ No newline at end of file diff --git a/src/main/java/org/labs/metrics/SimulationStats.java b/src/main/java/org/labs/metrics/SimulationStats.java new file mode 100644 index 0000000..104adf2 --- /dev/null +++ b/src/main/java/org/labs/metrics/SimulationStats.java @@ -0,0 +1,25 @@ +package org.labs.metrics; + +import org.labs.model.ProgrammerState; + +import java.util.Map; + +public final class SimulationStats { + /** Кол-во начавших есть/закончивших, очереди, отказов из-за отсутствия еды и т.д. */ + // Добавьте необходимые счетчики, таймеры, гистограммы latency по желанию. + + public void onProgrammerStateChange(int programmerId, ProgrammerState newState) { + // TODO + throw new UnsupportedOperationException("Not implemented"); + } + + public void onPortionConsumed(int programmerId) { + // TODO + throw new UnsupportedOperationException("Not implemented"); + } + + public Map summaryByProgrammer() { + // TODO + throw new UnsupportedOperationException("Not implemented"); + } +} \ No newline at end of file diff --git a/src/main/java/org/labs/model/ProgrammerState.java b/src/main/java/org/labs/model/ProgrammerState.java new file mode 100644 index 0000000..ae89e2a --- /dev/null +++ b/src/main/java/org/labs/model/ProgrammerState.java @@ -0,0 +1,10 @@ +package org.labs.model; + +public enum ProgrammerState { + THINKING, + WAITING_SPOONS, + EATING, + REQUESTING_REFILL, + WAITING_REFILL, + DONE +} \ No newline at end of file diff --git a/src/main/java/org/labs/model/RefillRequest.java b/src/main/java/org/labs/model/RefillRequest.java new file mode 100644 index 0000000..978dab0 --- /dev/null +++ b/src/main/java/org/labs/model/RefillRequest.java @@ -0,0 +1,14 @@ +package org.labs.model; + +public final class RefillRequest { + private final int programmerId; + private final long createdAtNanos; + + public RefillRequest(int programmerId, long createdAtNanos) { + this.programmerId = programmerId; + this.createdAtNanos = createdAtNanos; + } + + public int programmerId() { return programmerId; } + public long createdAtNanos() { return createdAtNanos; } +} \ No newline at end of file diff --git a/src/main/java/org/labs/strategy/deadlock/DeadlockAvoidanceStrategy.java b/src/main/java/org/labs/strategy/deadlock/DeadlockAvoidanceStrategy.java new file mode 100644 index 0000000..17b1fcc --- /dev/null +++ b/src/main/java/org/labs/strategy/deadlock/DeadlockAvoidanceStrategy.java @@ -0,0 +1,9 @@ +package org.labs.strategy.deadlock; + +public interface DeadlockAvoidanceStrategy { + /** Вызвать перед захватом ложек. Можно блокировать, пока нельзя есть. */ + void beforeAcquire(int programmerId) throws InterruptedException; + + /** Вызвать после освобождения ложек. */ + void afterRelease(int programmerId); +} \ No newline at end of file diff --git a/src/main/java/org/labs/strategy/deadlock/NMinusOneDeadlockStrategy.java b/src/main/java/org/labs/strategy/deadlock/NMinusOneDeadlockStrategy.java new file mode 100644 index 0000000..8664718 --- /dev/null +++ b/src/main/java/org/labs/strategy/deadlock/NMinusOneDeadlockStrategy.java @@ -0,0 +1,17 @@ +package org.labs.strategy.deadlock; + +final class NMinusOneDeadlockStrategy implements DeadlockAvoidanceStrategy { + public NMinusOneDeadlockStrategy(int participants) { + // TODO: инициализация семафора на (participants - 1) + } + + @Override + public void beforeAcquire(int programmerId) throws InterruptedException { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void afterRelease(int programmerId) { + throw new UnsupportedOperationException("Not implemented"); + } +} \ No newline at end of file diff --git a/src/main/java/org/labs/strategy/deadlock/OrderedSpoonsDeadlockStrategy.java b/src/main/java/org/labs/strategy/deadlock/OrderedSpoonsDeadlockStrategy.java new file mode 100644 index 0000000..b276d2c --- /dev/null +++ b/src/main/java/org/labs/strategy/deadlock/OrderedSpoonsDeadlockStrategy.java @@ -0,0 +1,13 @@ +package org.labs.strategy.deadlock; + +final class OrderedSpoonsDeadlockStrategy implements DeadlockAvoidanceStrategy { + @Override + public void beforeAcquire(int programmerId) throws InterruptedException { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void afterRelease(int programmerId) { + throw new UnsupportedOperationException("Not implemented"); + } +} diff --git a/src/main/java/org/labs/strategy/fairness/EqualShareFairness.java b/src/main/java/org/labs/strategy/fairness/EqualShareFairness.java new file mode 100644 index 0000000..de67064 --- /dev/null +++ b/src/main/java/org/labs/strategy/fairness/EqualShareFairness.java @@ -0,0 +1,25 @@ +package org.labs.strategy.fairness; + +import java.util.Map; + +final class EqualShareFairness implements FairnessStrategy { + @Override + public boolean tryEnterEat(int programmerId, long alreadyEaten) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void onStartEat(int programmerId) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void onFinishEat(int programmerId) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Map portionsByProgrammer() { + throw new UnsupportedOperationException("Not implemented"); + } +} \ No newline at end of file diff --git a/src/main/java/org/labs/strategy/fairness/FairnessStrategy.java b/src/main/java/org/labs/strategy/fairness/FairnessStrategy.java new file mode 100644 index 0000000..e1dedbc --- /dev/null +++ b/src/main/java/org/labs/strategy/fairness/FairnessStrategy.java @@ -0,0 +1,17 @@ +package org.labs.strategy.fairness; + +import java.util.Map; + +public interface FairnessStrategy { + /** Разрешить ли программисту начинать есть прямо сейчас. Может блокировать или возвращать false. */ + boolean tryEnterEat(int programmerId, long alreadyEaten); + + /** Сигнал о начале еды. */ + void onStartEat(int programmerId); + + /** Сигнал об окончании еды. */ + void onFinishEat(int programmerId); + + /** Текущая статистика порций по программистам. */ + Map portionsByProgrammer(); +} \ No newline at end of file diff --git a/src/main/java/org/labs/waiter/DefaultWaiter.java b/src/main/java/org/labs/waiter/DefaultWaiter.java new file mode 100644 index 0000000..8af6746 --- /dev/null +++ b/src/main/java/org/labs/waiter/DefaultWaiter.java @@ -0,0 +1,40 @@ +package org.labs.waiter; + +import org.labs.core.stock.FoodStock; +import org.labs.model.RefillRequest; + +import java.util.concurrent.BlockingQueue; + +final class DefaultWaiter implements Waiter { + private final int id; + private final BlockingQueue queue; + private final FoodStock stock; + private volatile boolean running = true; + + public DefaultWaiter(int id, BlockingQueue queue, FoodStock stock) { + this.id = id; + this.queue = queue; + this.stock = stock; + } + + @Override + public int id() { return id; } + + @Override + public void run() { + // TODO: poll из queue, handle(request) пока running + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void handle(RefillRequest request) { + // TODO: попытаться выдать 1 порцию из stock, уведомить программиста при успехе/неуспехе + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void shutdown() { + // TODO: остановка цикла + очистка + throw new UnsupportedOperationException("Not implemented"); + } +} \ No newline at end of file diff --git a/src/main/java/org/labs/waiter/Waiter.java b/src/main/java/org/labs/waiter/Waiter.java new file mode 100644 index 0000000..e72ca58 --- /dev/null +++ b/src/main/java/org/labs/waiter/Waiter.java @@ -0,0 +1,14 @@ +package org.labs.waiter; + + +import org.labs.model.RefillRequest; + +public interface Waiter extends Runnable { + int id(); + + /** Обрабатывает один запрос (внутри run-цикла). */ + void handle(RefillRequest request); + + /** Завершить работу (graceful). */ + void shutdown(); +} \ No newline at end of file diff --git a/src/test/java/SimulationTest.java b/src/test/java/SimulationTest.java new file mode 100644 index 0000000..75e0034 --- /dev/null +++ b/src/test/java/SimulationTest.java @@ -0,0 +1,2 @@ +public class SimulationTest { +} diff --git a/src/test/java/core/FoodStockTest.java b/src/test/java/core/FoodStockTest.java new file mode 100644 index 0000000..c34a018 --- /dev/null +++ b/src/test/java/core/FoodStockTest.java @@ -0,0 +1,4 @@ +package core; + +public class FoodStockTest { +} diff --git a/src/test/java/core/SpoonTest.java b/src/test/java/core/SpoonTest.java new file mode 100644 index 0000000..a20fff2 --- /dev/null +++ b/src/test/java/core/SpoonTest.java @@ -0,0 +1,4 @@ +package core; + +public class SpoonTest { +} diff --git a/src/test/java/strategy/DeadlockStrategiesTest.java b/src/test/java/strategy/DeadlockStrategiesTest.java new file mode 100644 index 0000000..ee2d307 --- /dev/null +++ b/src/test/java/strategy/DeadlockStrategiesTest.java @@ -0,0 +1,4 @@ +package strategy; + +public class DeadlockStrategiesTest { +} diff --git a/src/test/java/strategy/FairnessStrategiesTest.java b/src/test/java/strategy/FairnessStrategiesTest.java new file mode 100644 index 0000000..25109e4 --- /dev/null +++ b/src/test/java/strategy/FairnessStrategiesTest.java @@ -0,0 +1,4 @@ +package strategy; + +public class FairnessStrategiesTest { +} From bac1a191a4afd486b8ec09eb5083e7779b00234d Mon Sep 17 00:00:00 2001 From: poma12390 Date: Tue, 23 Sep 2025 17:06:38 +0300 Subject: [PATCH 3/7] lab1 done --- src/main/java/org/labs/Main.java | 14 - src/main/java/org/labs/app/Simulation.java | 249 +++++++++++++++++- .../org/labs/config/SimulationConfig.java | 128 ++++++++- src/main/java/org/labs/core/DiningTable.java | 127 +++++++-- .../java/org/labs/core/actors/Programmer.java | 187 ++++++++++--- .../org/labs/core/resources/LockingSpoon.java | 33 ++- .../java/org/labs/core/resources/Spoon.java | 9 +- .../org/labs/core/stock/AtomicFoodStock.java | 28 +- .../java/org/labs/core/stock/FoodStock.java | 6 +- .../org/labs/metrics/SimulationStats.java | 68 ++++- .../java/org/labs/model/RefillRequest.java | 33 ++- .../deadlock/DeadlockAvoidanceStrategy.java | 2 - .../deadlock/NMinusOneDeadlockStrategy.java | 25 +- .../OrderedSpoonsDeadlockStrategy.java | 48 +++- .../strategy/fairness/EqualShareFairness.java | 51 +++- .../strategy/fairness/FairnessStrategy.java | 11 +- .../java/org/labs/waiter/DefaultWaiter.java | 55 +++- src/main/java/org/labs/waiter/Waiter.java | 4 - .../java/org/labs/core/DiningTableTest.java | 125 +++++++++ .../org/labs/core/actors/ProgrammerTest.java | 78 ++++++ .../labs/core/resources/LockingSpoonTest.java | 47 ++++ .../labs/core/stock/AtomicFoodStockTest.java | 53 ++++ .../NMinusOneDeadlockStrategyTest.java | 44 ++++ .../OrderedSpoonsDeadlockStrategyTest.java | 32 +++ .../fairness/EqualShareFairnessTest.java | 39 +++ .../org/labs/waiter/DefaultWaiterTest.java | 40 +++ 26 files changed, 1372 insertions(+), 164 deletions(-) delete mode 100644 src/main/java/org/labs/Main.java create mode 100644 src/test/java/org/labs/core/DiningTableTest.java create mode 100644 src/test/java/org/labs/core/actors/ProgrammerTest.java create mode 100644 src/test/java/org/labs/core/resources/LockingSpoonTest.java create mode 100644 src/test/java/org/labs/core/stock/AtomicFoodStockTest.java create mode 100644 src/test/java/org/labs/strategy/deadlock/NMinusOneDeadlockStrategyTest.java create mode 100644 src/test/java/org/labs/strategy/deadlock/OrderedSpoonsDeadlockStrategyTest.java create mode 100644 src/test/java/org/labs/strategy/fairness/EqualShareFairnessTest.java create mode 100644 src/test/java/org/labs/waiter/DefaultWaiterTest.java diff --git a/src/main/java/org/labs/Main.java b/src/main/java/org/labs/Main.java deleted file mode 100644 index 534e12f..0000000 --- a/src/main/java/org/labs/Main.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.labs; - -public class Main { - public static void main(String[] args) { - // TODO: - // 1) собрать SimulationConfig из args/дефолтов - // 2) создать ложки (count = programmers) - // 3) создать shared BlockingQueue, FoodStock - // 4) выбрать стратегии DeadlockAvoidanceStrategy и FairnessStrategy - // 5) собрать официантов и программистов - // 6) собрать DiningTable и вызвать start()/awaitCompletion() - System.out.println("hello"); - } -} \ No newline at end of file diff --git a/src/main/java/org/labs/app/Simulation.java b/src/main/java/org/labs/app/Simulation.java index 67f2dbc..b7fbf56 100644 --- a/src/main/java/org/labs/app/Simulation.java +++ b/src/main/java/org/labs/app/Simulation.java @@ -1,4 +1,251 @@ package org.labs.app; -public class Simulation { +import org.labs.config.SimulationConfig; +import org.labs.core.DiningTable; +import org.labs.core.actors.Programmer; +import org.labs.core.resources.LockingSpoon; +import org.labs.core.resources.Spoon; +import org.labs.core.stock.AtomicFoodStock; +import org.labs.core.stock.FoodStock; +import org.labs.metrics.SimulationStats; +import org.labs.model.RefillRequest; +import org.labs.strategy.deadlock.DeadlockAvoidanceStrategy; +import org.labs.strategy.deadlock.NMinusOneDeadlockStrategy; +import org.labs.strategy.deadlock.OrderedSpoonsDeadlockStrategy; +import org.labs.strategy.fairness.EqualShareFairness; +import org.labs.strategy.fairness.FairnessStrategy; +import org.labs.waiter.DefaultWaiter; +import org.labs.waiter.Waiter; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +public final class Simulation { + + private static final ProgrammaticOverrides OVERRIDES = new ProgrammaticOverrides( + 7, // programmers (Integer) e.g. 7 + null, // waiters (Integer) e.g. 2 + 2000L, // totalPortions (Long) e.g. 1000 + null, null, // thinkMinMs, thinkMaxMs (Long) + null, null, // eatMinMs, eatMaxMs (Long) + null, // acquireTimeoutMs (Long) + null // fairShareRequired (Boolean) + ); + + public static void main(String[] args) { + Thread.setDefaultUncaughtExceptionHandler((t, e) -> + System.err.println("[FATAL] " + t.getName() + " crashed: " + e)); + + SimulationConfig cfg = buildConfigFromArgsOrOverrides(args); + + SimulationStats stats = new SimulationStats(); + FoodStock stock = new AtomicFoodStock(cfg.totalPortions()); + BlockingQueue refillQueue = new LinkedBlockingQueue<>(); + + DeadlockAvoidanceStrategy deadlock = chooseDeadlockStrategy(cfg, args); + FairnessStrategy fairness = chooseFairnessStrategy(cfg); + + int n = cfg.programmers(); + List spoons = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + spoons.add(new LockingSpoon(i)); + } + + List waiters = new ArrayList<>(cfg.waiters()); + List waiterThreads = new ArrayList<>(cfg.waiters()); + for (int i = 0; i < cfg.waiters(); i++) { + Waiter w = new DefaultWaiter(i, refillQueue, stock); + Thread wt = new Thread(w, "waiter-" + i); + waiters.add(w); + waiterThreads.add(wt); + } + + List programmers = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + Spoon left = spoons.get(i); + Spoon right = spoons.get((i + 1) % n); + programmers.add(new Programmer( + i, left, right, cfg, deadlock, fairness, refillQueue, stock, stats + )); + } + + DiningTable table = new DiningTable( + cfg, spoons, programmers, waiters, waiterThreads, + refillQueue, stock, deadlock, fairness, stats + ); + + // таймер для принудительной остановки, во избежание вечной работы + Long maxSeconds = findLongArg(args); + Thread watchdog; + if (maxSeconds != null && maxSeconds > 0) { + long ms = maxSeconds * 1000L; + watchdog = new Thread(() -> { + try { + Thread.sleep(ms); + System.err.println("[WATCHDOG] Max run reached (" + maxSeconds + "s). Shutting down..."); + table.shutdownNow(); + } catch (InterruptedException ignored) { } + }, "watchdog"); + watchdog.setDaemon(true); + watchdog.start(); + } + + long t0 = System.nanoTime(); + table.start(); + + try { + table.awaitCompletion(); + } catch (InterruptedException e) { + System.err.println("[MAIN] Interrupted while awaiting completion, shutting down..."); + Thread.currentThread().interrupt(); + table.shutdownNow(); + } + long t1 = System.nanoTime(); + + printSummary(stats, t0, t1); + } + + private static SimulationConfig buildConfigFromArgsOrOverrides(String[] args) { + if (args != null && args.length > 0) { + return buildConfigFromArgs(args); + } + + SimulationConfig.Builder b = SimulationConfig.builder(); + + if (Simulation.OVERRIDES.programmers != null) b.programmers(Simulation.OVERRIDES.programmers); + if (Simulation.OVERRIDES.waiters != null) b.waiters(Simulation.OVERRIDES.waiters); + if (Simulation.OVERRIDES.totalPortions != null) b.totalPortions(Simulation.OVERRIDES.totalPortions); + + if (Simulation.OVERRIDES.thinkMinMs != null || Simulation.OVERRIDES.thinkMaxMs != null) { + long min = Simulation.OVERRIDES.thinkMinMs != null ? Simulation.OVERRIDES.thinkMinMs : b.build().minThink().toMillis(); + long max = Simulation.OVERRIDES.thinkMaxMs != null ? Simulation.OVERRIDES.thinkMaxMs : b.build().maxThink().toMillis(); + b.thinkRange(Duration.ofMillis(min), Duration.ofMillis(max)); + } + + if (Simulation.OVERRIDES.eatMinMs != null || Simulation.OVERRIDES.eatMaxMs != null) { + long min = Simulation.OVERRIDES.eatMinMs != null ? Simulation.OVERRIDES.eatMinMs : b.build().minEat().toMillis(); + long max = Simulation.OVERRIDES.eatMaxMs != null ? Simulation.OVERRIDES.eatMaxMs : b.build().maxEat().toMillis(); + b.eatRange(Duration.ofMillis(min), Duration.ofMillis(max)); + } + + if (Simulation.OVERRIDES.acquireTimeoutMs != null) { + b.spoonAcquireTimeout(Duration.ofMillis(Simulation.OVERRIDES.acquireTimeoutMs)); + } + + if (Simulation.OVERRIDES.fairShareRequired != null) { + b.fairShareRequired(Simulation.OVERRIDES.fairShareRequired); + } + + return b.build(); + } + + private static SimulationConfig buildConfigFromArgs(String[] args) { + SimulationConfig.Builder b = SimulationConfig.builder(); + + for (int i = 0; i < args.length; i++) { + switch (args[i]) { + case "-n": + case "--programmers": + b.programmers(Integer.parseInt(next(args, ++i, "programmers"))); + break; + case "-w": + case "--waiters": + b.waiters(Integer.parseInt(next(args, ++i, "waiters"))); + break; + case "-p": + case "--portions": + b.totalPortions(Long.parseLong(next(args, ++i, "portions"))); + break; + case "--think": { + String[] mm = next(args, ++i, "think").split(","); + b.thinkRange(Duration.ofMillis(Long.parseLong(mm[0])), Duration.ofMillis(Long.parseLong(mm[1]))); + break; + } + case "--eat": { + String[] mm = next(args, ++i, "eat").split(","); + b.eatRange(Duration.ofMillis(Long.parseLong(mm[0])), Duration.ofMillis(Long.parseLong(mm[1]))); + break; + } + case "--acquire-timeout": + b.spoonAcquireTimeout(Duration.ofMillis(Long.parseLong(next(args, ++i, "acquire-timeout")))); + break; + case "--no-fairness": + b.fairShareRequired(false); + break; + default: + break; + } + } + + return b.build(); + } + + private static DeadlockAvoidanceStrategy chooseDeadlockStrategy(SimulationConfig cfg, String[] args) { + String mode = null; + for (int i = 0; i < args.length; i++) { + if ("--strategy".equals(args[i]) && i + 1 < args.length) { + mode = args[i + 1]; + break; + } + } + if ("ordered".equalsIgnoreCase(mode)) { + return new OrderedSpoonsDeadlockStrategy(); + } + return new NMinusOneDeadlockStrategy(cfg.programmers()); + } + + private static FairnessStrategy chooseFairnessStrategy(SimulationConfig cfg) { + if (!cfg.fairShareRequired()) return new NoopFairness(); + return new EqualShareFairness(1); + } + + private static String next(String[] a, int idx, String name) { + if (idx >= a.length) throw new IllegalArgumentException("Missing value for " + name); + return a[idx]; + } + + private static Long findLongArg(String[] args) { + for (int i = 0; i < args.length; i++) { + if ("--max-seconds".equals(args[i]) && i + 1 < args.length) { + try { return Long.parseLong(args[i + 1]); } + catch (NumberFormatException ignore) { return null; } + } + } + return null; + } + + private static void printSummary(SimulationStats stats, long t0, long t1) { + double sec = (t1 - t0) / 1_000_000_000.0; + System.out.println("\n=== Simulation Summary ==="); + System.out.printf("Elapsed: %.3f s%n", sec); + System.out.println("Total consumed: " + stats.totalConsumed()); + System.out.println("State distribution: " + stats.stateDistribution()); + var perProg = stats.summaryByProgrammer(); + System.out.println("Per-programmer portions:"); + perProg.entrySet().stream() + .sorted(Comparator.comparingInt(Map.Entry::getKey)) + .forEach(e -> System.out.printf(" #%d : %d%n", e.getKey(), e.getValue())); + } + + + private static final class NoopFairness implements FairnessStrategy { + @Override public boolean tryEnterEat(int programmerId, long alreadyEaten) { return true; } + @Override public void onStartEat(int programmerId) { } + @Override public void onFinishEat(int programmerId) { } + } + + private record ProgrammaticOverrides( + Integer programmers, + Integer waiters, + Long totalPortions, + Long thinkMinMs, Long thinkMaxMs, + Long eatMinMs, Long eatMaxMs, + Long acquireTimeoutMs, + Boolean fairShareRequired + ) { } } diff --git a/src/main/java/org/labs/config/SimulationConfig.java b/src/main/java/org/labs/config/SimulationConfig.java index 7177d98..c0b57b5 100644 --- a/src/main/java/org/labs/config/SimulationConfig.java +++ b/src/main/java/org/labs/config/SimulationConfig.java @@ -1,16 +1,26 @@ package org.labs.config; import java.time.Duration; +import java.util.Objects; +/** + * Неизменяемая конфигурация симуляции. + * Создаётся через {@link Builder}. Все поля валидируются в {@link Builder#build()}. + */ public final class SimulationConfig { + private final int programmers; private final int waiters; private final long totalPortions; + private final Duration minThink; private final Duration maxThink; + private final Duration minEat; private final Duration maxEat; + private final Duration spoonAcquireTimeout; + private final boolean fairShareRequired; private SimulationConfig( @@ -38,39 +48,133 @@ private SimulationConfig( public int programmers() { return programmers; } public int waiters() { return waiters; } public long totalPortions() { return totalPortions; } + public Duration minThink() { return minThink; } public Duration maxThink() { return maxThink; } + public Duration minEat() { return minEat; } public Duration maxEat() { return maxEat; } + public Duration spoonAcquireTimeout() { return spoonAcquireTimeout; } + public boolean fairShareRequired() { return fairShareRequired; } public static Builder builder() { return new Builder(); } + @Override + public String toString() { + return "SimulationConfig{" + + "programmers=" + programmers + + ", waiters=" + waiters + + ", totalPortions=" + totalPortions + + ", thinkRange=" + minThink + ".." + maxThink + + ", eatRange=" + minEat + ".." + maxEat + + ", spoonAcquireTimeout=" + spoonAcquireTimeout + + ", fairShareRequired=" + fairShareRequired + + '}'; + } + + public static final class Builder { private int programmers = 7; private int waiters = 2; - private long totalPortions = 1_000_000L; + private long totalPortions = 1000; + private Duration minThink = Duration.ofMillis(50); private Duration maxThink = Duration.ofMillis(200); - private Duration minEat = Duration.ofMillis(50); - private Duration maxEat = Duration.ofMillis(200); + + private Duration minEat = Duration.ofMillis(50); + private Duration maxEat = Duration.ofMillis(200); + private Duration spoonAcquireTimeout = Duration.ofMillis(250); + private boolean fairShareRequired = true; - public Builder programmers(int v) { this.programmers = v; return this; } - public Builder waiters(int v) { this.waiters = v; return this; } - public Builder totalPortions(long v) { this.totalPortions = v; return this; } - public Builder thinkRange(Duration min, Duration max) { this.minThink = min; this.maxThink = max; return this; } - public Builder eatRange(Duration min, Duration max) { this.minEat = min; this.maxEat = max; return this; } - public Builder spoonAcquireTimeout(Duration v) { this.spoonAcquireTimeout = v; return this; } - public Builder fairShareRequired(boolean v) { this.fairShareRequired = v; return this; } + public Builder programmers(int v) { + this.programmers = v; + return this; + } + + public Builder waiters(int v) { + this.waiters = v; + return this; + } + + public Builder totalPortions(long v) { + this.totalPortions = v; + return this; + } + + public Builder thinkRange(Duration min, Duration max) { + this.minThink = Objects.requireNonNull(min, "minThink"); + this.maxThink = Objects.requireNonNull(max, "maxThink"); + return this; + } + + public Builder eatRange(Duration min, Duration max) { + this.minEat = Objects.requireNonNull(min, "minEat"); + this.maxEat = Objects.requireNonNull(max, "maxEat"); + return this; + } + + public Builder spoonAcquireTimeout(Duration v) { + this.spoonAcquireTimeout = Objects.requireNonNull(v, "spoonAcquireTimeout"); + return this; + } + + public Builder fairShareRequired(boolean v) { + this.fairShareRequired = v; + return this; + } public SimulationConfig build() { - // validate inputs (N>1, waiters>0, etc.) + if (programmers < 2) { + throw new IllegalArgumentException("programmers must be >= 2"); + } + if (waiters < 1) { + throw new IllegalArgumentException("waiters must be >= 1"); + } + if (totalPortions < 1) { + throw new IllegalArgumentException("totalPortions must be >= 1"); + } + + requireNonNullDurations(); + + if (minThink.isNegative() || maxThink.isNegative()) { + throw new IllegalArgumentException("think durations must be >= 0"); + } + if (minEat.isNegative() || maxEat.isNegative()) { + throw new IllegalArgumentException("eat durations must be >= 0"); + } + if (minThink.compareTo(maxThink) > 0) { + throw new IllegalArgumentException("minThink must be <= maxThink"); + } + if (minEat.compareTo(maxEat) > 0) { + throw new IllegalArgumentException("minEat must be <= maxEat"); + } + if (spoonAcquireTimeout.isNegative() || spoonAcquireTimeout.isZero()) { + throw new IllegalArgumentException("spoonAcquireTimeout must be > 0"); + } + return new SimulationConfig( - programmers, waiters, totalPortions, minThink, maxThink, minEat, maxEat, spoonAcquireTimeout, fairShareRequired + programmers, + waiters, + totalPortions, + minThink, + maxThink, + minEat, + maxEat, + spoonAcquireTimeout, + fairShareRequired ); } + + private void requireNonNullDurations() { + Objects.requireNonNull(minThink, "minThink"); + Objects.requireNonNull(maxThink, "maxThink"); + Objects.requireNonNull(minEat, "minEat"); + Objects.requireNonNull(maxEat, "maxEat"); + Objects.requireNonNull(spoonAcquireTimeout, "spoonAcquireTimeout"); + } } } \ No newline at end of file diff --git a/src/main/java/org/labs/core/DiningTable.java b/src/main/java/org/labs/core/DiningTable.java index 0fe4f1c..743f0d5 100644 --- a/src/main/java/org/labs/core/DiningTable.java +++ b/src/main/java/org/labs/core/DiningTable.java @@ -10,7 +10,10 @@ import org.labs.strategy.fairness.FairnessStrategy; import org.labs.waiter.Waiter; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.BlockingQueue; public final class DiningTable { @@ -25,6 +28,11 @@ public final class DiningTable { private final FairnessStrategy fairness; private final SimulationStats stats; + private final List programmerThreads = new ArrayList<>(); + + private volatile boolean started = false; + private volatile boolean stopped = false; + public DiningTable( SimulationConfig cfg, List spoons, @@ -37,35 +45,110 @@ public DiningTable( FairnessStrategy fairness, SimulationStats stats ) { - this.cfg = cfg; - this.spoons = spoons; - this.programmers = programmers; - this.waiters = waiters; - this.waiterThreads = waiterThreads; - this.refillQueue = refillQueue; - this.stock = stock; - this.deadlockStrategy = deadlockStrategy; - this.fairness = fairness; - this.stats = stats; + this.cfg = Objects.requireNonNull(cfg, "cfg"); + this.spoons = List.copyOf(Objects.requireNonNull(spoons, "spoons")); + this.programmers = List.copyOf(Objects.requireNonNull(programmers, "programmers")); + this.waiters = List.copyOf(Objects.requireNonNull(waiters, "waiters")); + this.waiterThreads = new ArrayList<>(Objects.requireNonNull(waiterThreads, "waiterThreads")); + this.refillQueue = Objects.requireNonNull(refillQueue, "refillQueue"); + this.stock = Objects.requireNonNull(stock, "stock"); + this.deadlockStrategy = Objects.requireNonNull(deadlockStrategy, "deadlockStrategy"); + this.fairness = Objects.requireNonNull(fairness, "fairness"); + this.stats = Objects.requireNonNull(stats, "stats"); + + if (this.waiters.size() != this.waiterThreads.size()) { + throw new IllegalArgumentException("waiters and waiterThreads sizes must match"); + } + if (this.spoons.size() < this.programmers.size()) { + throw new IllegalArgumentException("spoons count must be >= programmers count"); + } + if (cfg.programmers() != this.programmers.size()) { + throw new IllegalArgumentException("cfg.programmers must equal provided programmers size"); + } } - /** Запуск всех потоков. */ - public void start() { - // TODO: старт официантов и программистов - throw new UnsupportedOperationException("Not implemented"); + public synchronized void start() { + ensureNotStopped(); + if (started) { + throw new IllegalStateException("DiningTable already started"); + } + started = true; + + for (int i = 0; i < waiterThreads.size(); i++) { + Thread t = waiterThreads.get(i); + if (!t.isAlive()) { + String name = "waiter-" + i; + if (t.getName() == null || t.getName().isBlank()) { + t.setName(name); + } + t.start(); + } + } + + for (Programmer p : programmers) { + Thread t = new Thread(p, "programmer-" + p.id()); + programmerThreads.add(t); + t.start(); + } } - /** Дождаться завершения (нет еды или достигнуты критерии остановки). */ public void awaitCompletion() throws InterruptedException { - // TODO: join потоков - throw new UnsupportedOperationException("Not implemented"); + ensureStarted(); + + for (Thread t : programmerThreads) { + t.join(); + } + + shutdownWaitersGracefully(); + + for (Thread t : waiterThreads) { + t.join(); + } + + stopped = true; + } + + public synchronized void shutdownNow() { + if (stopped) return; + for (Waiter w : waiters) { + try { + w.shutdown(); + } catch (Throwable ignored) {} + } + for (Thread t : programmerThreads) { + t.interrupt(); + } + for (Thread t : waiterThreads) { + t.interrupt(); + } + refillQueue.clear(); + stopped = true; + } + + private void shutdownWaitersGracefully() { + for (Waiter w : waiters) { + try { + w.shutdown(); + } catch (Throwable ignored) {} + } + for (Thread t : waiterThreads) { + t.interrupt(); + } + } + + private void ensureStarted() { + if (!started) { + throw new IllegalStateException("DiningTable not started"); + } } - /** Аварийная остановка. */ - public void shutdownNow() { - // TODO: прервать все потоки, закрыть ресурсы - throw new UnsupportedOperationException("Not implemented"); + private void ensureNotStopped() { + if (stopped) { + throw new IllegalStateException("DiningTable already stopped"); + } } - public SimulationStats stats() { return stats; } + public List programmerThreads() { return Collections.unmodifiableList(programmerThreads); } + public List waiterThreads() { return Collections.unmodifiableList(waiterThreads); } + } \ No newline at end of file diff --git a/src/main/java/org/labs/core/actors/Programmer.java b/src/main/java/org/labs/core/actors/Programmer.java index 6478f57..77e0391 100644 --- a/src/main/java/org/labs/core/actors/Programmer.java +++ b/src/main/java/org/labs/core/actors/Programmer.java @@ -2,12 +2,18 @@ import org.labs.config.SimulationConfig; import org.labs.core.resources.Spoon; +import org.labs.core.stock.FoodStock; +import org.labs.metrics.SimulationStats; import org.labs.model.ProgrammerState; import org.labs.model.RefillRequest; import org.labs.strategy.deadlock.DeadlockAvoidanceStrategy; import org.labs.strategy.fairness.FairnessStrategy; +import java.time.Duration; +import java.util.Objects; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; public final class Programmer implements Runnable { private final int id; @@ -17,8 +23,12 @@ public final class Programmer implements Runnable { private final DeadlockAvoidanceStrategy deadlockStrategy; private final FairnessStrategy fairness; private final BlockingQueue refillQueue; + private final FoodStock stock; + private final SimulationStats stats; // может быть null, если метрики не нужны + private volatile ProgrammerState state = ProgrammerState.THINKING; - private long portionsEaten; + private long portionsEaten = 0; + private boolean hasPortion = false; // локальная тарелка: есть ли непросаженная порция public Programmer( int id, @@ -27,50 +37,165 @@ public Programmer( SimulationConfig cfg, DeadlockAvoidanceStrategy deadlockStrategy, FairnessStrategy fairness, - BlockingQueue refillQueue + BlockingQueue refillQueue, + FoodStock stock, + SimulationStats stats ) { + if (id < 0) throw new IllegalArgumentException("id must be >= 0"); this.id = id; - this.left = left; - this.right = right; - this.cfg = cfg; - this.deadlockStrategy = deadlockStrategy; - this.fairness = fairness; - this.refillQueue = refillQueue; + this.left = Objects.requireNonNull(left, "left"); + this.right = Objects.requireNonNull(right, "right"); + this.cfg = Objects.requireNonNull(cfg, "cfg"); + this.deadlockStrategy = Objects.requireNonNull(deadlockStrategy, "deadlockStrategy"); + this.fairness = Objects.requireNonNull(fairness, "fairness"); + this.refillQueue = Objects.requireNonNull(refillQueue, "refillQueue"); + this.stock = Objects.requireNonNull(stock, "stock"); + this.stats = stats; // допускаем null } public int id() { return id; } - public ProgrammerState state() { return state; } + public long portionsEaten() { return portionsEaten; } @Override public void run() { - // TODO: цикл пока есть еда в системе; - // - think() - // - deadlockStrategy.beforeAcquire(id) - // - попытка взять две ложки (учитывая таймаут из cfg) - // - fairness.tryEnterEat(...) - // - eat() - // - освободить ложки + deadlockStrategy.afterRelease(id) - // - если суп кончился в тарелке – requestRefill() - // Логи/метрики по состояниям. - throw new UnsupportedOperationException("Not implemented"); + try { + mainLoop(); + } finally { + updateState(ProgrammerState.DONE); + } } - /** Подумать/поболтать. */ - void think() { - // TODO: sleep в диапазоне cfg.minThink..cfg.maxThink - throw new UnsupportedOperationException("Not implemented"); + private void mainLoop() { + final long acquireTimeoutMs = Math.max(0, cfg.spoonAcquireTimeout().toMillis()); + + while (!Thread.currentThread().isInterrupted()) { + try { + think(); + + if (!hasPortion) { + boolean refilled = requestRefillAndAwait(); + if (!refilled) { + if (!hasPortion) { + break; + } + } + } + + boolean gateEntered = false; + try { + deadlockStrategy.beforeAcquire(id); + gateEntered = true; + + Spoon first = left.id() <= right.id() ? left : right; + Spoon second = left.id() <= right.id() ? right : left; + + updateState(ProgrammerState.WAITING_SPOONS); + if (!first.tryAcquire(acquireTimeoutMs, TimeUnit.MILLISECONDS)) { + continue; + } + + boolean secondAcquired = false; + try { + if (!second.tryAcquire(acquireTimeoutMs, TimeUnit.MILLISECONDS)) { + continue; + } + secondAcquired = true; + + if (!fairness.tryEnterEat(id, portionsEaten)) { + continue; + } + + fairness.onStartEat(id); + updateState(ProgrammerState.EATING); + eat(); + fairness.onFinishEat(id); + + } finally { + if (secondAcquired) { + try { second.release(); } catch (Throwable ignored) {} + } + try { first.release(); } catch (Throwable ignored) {} + } + + } finally { + if (gateEntered) { + try { deadlockStrategy.afterRelease(id); } catch (Throwable ignored) {} + } + } + + if (stock.isDepleted() && !hasPortion) { + break; + } + + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + } + + void think() throws InterruptedException { + updateState(ProgrammerState.THINKING); + randomSleepBetween(cfg.minThink(), cfg.maxThink()); + } + + void eat() throws InterruptedException { + if (!hasPortion) { + return; + } + randomSleepBetween(cfg.minEat(), cfg.maxEat()); + hasPortion = false; + portionsEaten++; + if (stats != null) { + stats.onPortionConsumed(id); + } + } + + boolean requestRefillAndAwait() throws InterruptedException { + updateState(ProgrammerState.REQUESTING_REFILL); + RefillRequest req = new RefillRequest(id); + updateState(ProgrammerState.WAITING_REFILL); + + refillQueue.put(req); + boolean granted; + granted = req.awaitResult(); + + if (granted) { + hasPortion = true; + return true; + } else { + hasPortion = false; + return false; + } } - /** Пожевать супчик (пока у программиста есть порция в тарелке). */ - void eat() { - // TODO: sleep в диапазоне cfg.minEat..cfg.maxEat; увеличить portionsEaten - throw new UnsupportedOperationException("Not implemented"); + private static void randomSleepBetween(Duration min, Duration max) throws InterruptedException { + long minMs = Math.max(0, min.toMillis()); + long maxMs = Math.max(minMs, max.toMillis()); + long span = maxMs - minMs; + long sleepMs = minMs + (span == 0 ? 0 : ThreadLocalRandom.current().nextLong(span + 1)); + if (sleepMs > 0) { + Thread.sleep(sleepMs); + } else { + Thread.onSpinWait(); + } } - /** Попросить долив у официанта. */ - void requestRefill() { - // TODO: добавить RefillRequest в refillQueue - throw new UnsupportedOperationException("Not implemented"); + private void updateState(ProgrammerState newState) { + this.state = newState; + if (stats != null) { + stats.onProgrammerStateChange(id, newState); + } + } + + @Override + public String toString() { + return "Programmer{" + + "id=" + id + + ", state=" + state + + ", portionsEaten=" + portionsEaten + + ", hasPortion=" + hasPortion + + '}'; } } \ No newline at end of file diff --git a/src/main/java/org/labs/core/resources/LockingSpoon.java b/src/main/java/org/labs/core/resources/LockingSpoon.java index 6d9b201..01449dc 100644 --- a/src/main/java/org/labs/core/resources/LockingSpoon.java +++ b/src/main/java/org/labs/core/resources/LockingSpoon.java @@ -1,30 +1,41 @@ package org.labs.core.resources; -import java.util.Optional; +import java.util.Objects; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; -final class LockingSpoon implements Spoon { +public final class LockingSpoon implements Spoon { private final int id; + private final ReentrantLock lock; public LockingSpoon(int id) { - this.id = id; + this(id, false); } - @Override public int id() { return id; } + public LockingSpoon(int id, boolean fair) { + if (id < 0) throw new IllegalArgumentException("id must be >= 0"); + this.id = id; + this.lock = new ReentrantLock(fair); + } @Override - public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { - throw new UnsupportedOperationException("Not implemented"); + public int id() { + return id; } @Override - public void release() { - throw new UnsupportedOperationException("Not implemented"); + public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { + if (timeout < 0) throw new IllegalArgumentException("timeout must be >= 0"); + Objects.requireNonNull(unit, "unit"); + return lock.tryLock(timeout, unit); } @Override - public Optional underlyingLock() { - return Optional.empty(); + public void release() { + if (!lock.isHeldByCurrentThread()) { + throw new IllegalStateException("Thread " + Thread.currentThread().getName() + + " attempted to release spoon #" + id + " without owning its lock"); + } + lock.unlock(); } } \ No newline at end of file diff --git a/src/main/java/org/labs/core/resources/Spoon.java b/src/main/java/org/labs/core/resources/Spoon.java index 41b09ae..be0631d 100644 --- a/src/main/java/org/labs/core/resources/Spoon.java +++ b/src/main/java/org/labs/core/resources/Spoon.java @@ -1,21 +1,16 @@ package org.labs.core.resources; -import java.util.Optional; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; public interface Spoon { int id(); /** - * Попытаться захватить ложку в течение таймаута. + * Попытаться захватить ложку в течение таймаута * @return true если захвачена, иначе false */ boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException; - /** Освободить ложку. */ + /** Освободить ложку */ void release(); - - /** Для метрик/отладки – предоставьте доступ к базовой блокировке при необходимости. */ - Optional underlyingLock(); } \ No newline at end of file diff --git a/src/main/java/org/labs/core/stock/AtomicFoodStock.java b/src/main/java/org/labs/core/stock/AtomicFoodStock.java index 36baefa..3d69706 100644 --- a/src/main/java/org/labs/core/stock/AtomicFoodStock.java +++ b/src/main/java/org/labs/core/stock/AtomicFoodStock.java @@ -1,17 +1,37 @@ package org.labs.core.stock; -final class AtomicFoodStock implements FoodStock { +import java.util.concurrent.atomic.AtomicLong; + +public final class AtomicFoodStock implements FoodStock { + private final AtomicLong portions; + public AtomicFoodStock(long initialPortions) { - // TODO: init atomic counter + if (initialPortions < 0) { + throw new IllegalArgumentException("initialPortions must be >= 0"); + } + this.portions = new AtomicLong(initialPortions); } @Override public boolean tryTakeOne() { - throw new UnsupportedOperationException("Not implemented"); + while (true) { + long current = portions.get(); + if (current <= 0) { + return false; + } + if (portions.compareAndSet(current, current - 1)) { + return true; + } + } } @Override public long remaining() { - throw new UnsupportedOperationException("Not implemented"); + return portions.get(); + } + + @Override + public String toString() { + return "AtomicFoodStock{remaining=" + portions.get() + '}'; } } \ No newline at end of file diff --git a/src/main/java/org/labs/core/stock/FoodStock.java b/src/main/java/org/labs/core/stock/FoodStock.java index b8546fa..884731d 100644 --- a/src/main/java/org/labs/core/stock/FoodStock.java +++ b/src/main/java/org/labs/core/stock/FoodStock.java @@ -2,13 +2,13 @@ public interface FoodStock { /** - * Попытаться выдать 1 порцию. Возвращает true, если удалось уменьшить запас. + * Попытаться выдать 1 порцию. Возвращает true, если удалось уменьшить запас */ boolean tryTakeOne(); - /** Сколько порций осталось. */ + /** Сколько порций осталось */ long remaining(); - /** Признак завершения – запаса больше нет. */ + /** Признак завершения */ default boolean isDepleted() { return remaining() <= 0; } } \ No newline at end of file diff --git a/src/main/java/org/labs/metrics/SimulationStats.java b/src/main/java/org/labs/metrics/SimulationStats.java index 104adf2..15100cc 100644 --- a/src/main/java/org/labs/metrics/SimulationStats.java +++ b/src/main/java/org/labs/metrics/SimulationStats.java @@ -2,24 +2,76 @@ import org.labs.model.ProgrammerState; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.LongAdder; public final class SimulationStats { - /** Кол-во начавших есть/закончивших, очереди, отказов из-за отсутствия еды и т.д. */ - // Добавьте необходимые счетчики, таймеры, гистограммы latency по желанию. + + private final ConcurrentMap states = new ConcurrentHashMap<>(); + private final EnumMap stateDistribution = + new EnumMap<>(ProgrammerState.class); + + private final ConcurrentMap portionsByProgrammer = new ConcurrentHashMap<>(); + private final LongAdder totalPortions = new LongAdder(); + + private final long startedAtNanos = System.nanoTime(); + private volatile long lastStateChangeNanos = startedAtNanos; + private volatile long lastPortionNanos = 0L; + + public SimulationStats() { + for (ProgrammerState st : ProgrammerState.values()) { + stateDistribution.put(st, new LongAdder()); + } + } public void onProgrammerStateChange(int programmerId, ProgrammerState newState) { - // TODO - throw new UnsupportedOperationException("Not implemented"); + Objects.requireNonNull(newState, "newState"); + ProgrammerState prev = states.put(programmerId, newState); + if (prev != null) { + stateDistribution.get(prev).decrement(); + } + stateDistribution.get(newState).increment(); + lastStateChangeNanos = System.nanoTime(); } public void onPortionConsumed(int programmerId) { - // TODO - throw new UnsupportedOperationException("Not implemented"); + portionsByProgrammer + .computeIfAbsent(programmerId, k -> new LongAdder()) + .increment(); + totalPortions.increment(); + lastPortionNanos = System.nanoTime(); } public Map summaryByProgrammer() { - // TODO - throw new UnsupportedOperationException("Not implemented"); + Map snapshot = new HashMap<>(); + portionsByProgrammer.forEach((id, adder) -> snapshot.put(id, adder.longValue())); + return Collections.unmodifiableMap(snapshot); + } + + public Map stateDistribution() { + Map m = new EnumMap<>(ProgrammerState.class); + for (Map.Entry e : stateDistribution.entrySet()) { + m.put(e.getKey(), e.getValue().longValue()); + } + return Collections.unmodifiableMap(m); + } + + public long totalConsumed() { + return totalPortions.longValue(); + } + + @Override + public String toString() { + return "SimulationStats{" + + "totalPortions=" + totalPortions.longValue() + + ", stateDistribution=" + stateDistribution() + + ", perProgrammer=" + summaryByProgrammer() + + '}'; } } \ No newline at end of file diff --git a/src/main/java/org/labs/model/RefillRequest.java b/src/main/java/org/labs/model/RefillRequest.java index 978dab0..0a31178 100644 --- a/src/main/java/org/labs/model/RefillRequest.java +++ b/src/main/java/org/labs/model/RefillRequest.java @@ -1,14 +1,33 @@ package org.labs.model; +import java.util.concurrent.CompletableFuture; + public final class RefillRequest { - private final int programmerId; - private final long createdAtNanos; + private final CompletableFuture result; + + public RefillRequest(int programmerId) { + if (programmerId < 0) { + throw new IllegalArgumentException("programmerId must be >= 0"); + } + this.result = new CompletableFuture<>(); + } + + public CompletableFuture resultFuture() { + return result; + } + + public void tryComplete(boolean success) { + result.complete(success); + } - public RefillRequest(int programmerId, long createdAtNanos) { - this.programmerId = programmerId; - this.createdAtNanos = createdAtNanos; + public boolean awaitResult() throws InterruptedException { + try { + return result.get(); + } catch (InterruptedException ie) { + throw ie; + } catch (Exception e) { + throw new IllegalStateException("Unexpected completion state", e); + } } - public int programmerId() { return programmerId; } - public long createdAtNanos() { return createdAtNanos; } } \ No newline at end of file diff --git a/src/main/java/org/labs/strategy/deadlock/DeadlockAvoidanceStrategy.java b/src/main/java/org/labs/strategy/deadlock/DeadlockAvoidanceStrategy.java index 17b1fcc..1c03067 100644 --- a/src/main/java/org/labs/strategy/deadlock/DeadlockAvoidanceStrategy.java +++ b/src/main/java/org/labs/strategy/deadlock/DeadlockAvoidanceStrategy.java @@ -1,9 +1,7 @@ package org.labs.strategy.deadlock; public interface DeadlockAvoidanceStrategy { - /** Вызвать перед захватом ложек. Можно блокировать, пока нельзя есть. */ void beforeAcquire(int programmerId) throws InterruptedException; - /** Вызвать после освобождения ложек. */ void afterRelease(int programmerId); } \ No newline at end of file diff --git a/src/main/java/org/labs/strategy/deadlock/NMinusOneDeadlockStrategy.java b/src/main/java/org/labs/strategy/deadlock/NMinusOneDeadlockStrategy.java index 8664718..560f6c6 100644 --- a/src/main/java/org/labs/strategy/deadlock/NMinusOneDeadlockStrategy.java +++ b/src/main/java/org/labs/strategy/deadlock/NMinusOneDeadlockStrategy.java @@ -1,17 +1,32 @@ package org.labs.strategy.deadlock; -final class NMinusOneDeadlockStrategy implements DeadlockAvoidanceStrategy { +import java.util.concurrent.Semaphore; + +public final class NMinusOneDeadlockStrategy implements DeadlockAvoidanceStrategy { + private final Semaphore gate; + public NMinusOneDeadlockStrategy(int participants) { - // TODO: инициализация семафора на (participants - 1) + this(participants, true); + } + + public NMinusOneDeadlockStrategy(int participants, boolean fair) { + if (participants < 2) { + throw new IllegalArgumentException("participants must be >= 2"); + } + this.gate = new Semaphore(participants - 1, fair); } @Override public void beforeAcquire(int programmerId) throws InterruptedException { - throw new UnsupportedOperationException("Not implemented"); + gate.acquire(); } @Override public void afterRelease(int programmerId) { - throw new UnsupportedOperationException("Not implemented"); + gate.release(); + } + + public int availableSlots() { + return gate.availablePermits(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/labs/strategy/deadlock/OrderedSpoonsDeadlockStrategy.java b/src/main/java/org/labs/strategy/deadlock/OrderedSpoonsDeadlockStrategy.java index b276d2c..680d684 100644 --- a/src/main/java/org/labs/strategy/deadlock/OrderedSpoonsDeadlockStrategy.java +++ b/src/main/java/org/labs/strategy/deadlock/OrderedSpoonsDeadlockStrategy.java @@ -1,13 +1,51 @@ package org.labs.strategy.deadlock; -final class OrderedSpoonsDeadlockStrategy implements DeadlockAvoidanceStrategy { +import org.labs.core.resources.Spoon; + +import java.util.Objects; + +public final class OrderedSpoonsDeadlockStrategy implements DeadlockAvoidanceStrategy { + + public OrderedSpoonsDeadlockStrategy() { } + @Override - public void beforeAcquire(int programmerId) throws InterruptedException { - throw new UnsupportedOperationException("Not implemented"); + public void beforeAcquire(int programmerId) { + // no-op } @Override public void afterRelease(int programmerId) { - throw new UnsupportedOperationException("Not implemented"); + // no-op + } + + public Spoon first(Spoon left, Spoon right) { + Objects.requireNonNull(left, "left"); + Objects.requireNonNull(right, "right"); + return left.id() <= right.id() ? left : right; + } + + public Spoon second(Spoon left, Spoon right) { + Objects.requireNonNull(left, "left"); + Objects.requireNonNull(right, "right"); + return left.id() <= right.id() ? right : left; + } + + public OrderedPair order(Spoon a, Spoon b) { + Objects.requireNonNull(a, "a"); + Objects.requireNonNull(b, "b"); + return a.id() <= b.id() ? new OrderedPair(a, b) : new OrderedPair(b, a); + } + + public static final class OrderedPair { + private final Spoon first; + private final Spoon second; + + private OrderedPair(Spoon first, Spoon second) { + this.first = first; + this.second = second; + } + + public Spoon first() { return first; } + public Spoon second() { return second; } } -} +} \ No newline at end of file diff --git a/src/main/java/org/labs/strategy/fairness/EqualShareFairness.java b/src/main/java/org/labs/strategy/fairness/EqualShareFairness.java index de67064..143c5d2 100644 --- a/src/main/java/org/labs/strategy/fairness/EqualShareFairness.java +++ b/src/main/java/org/labs/strategy/fairness/EqualShareFairness.java @@ -1,25 +1,60 @@ package org.labs.strategy.fairness; -import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; + +public final class EqualShareFairness implements FairnessStrategy { + + private final int slack; + + private final ConcurrentMap finishedPerProgrammer = new ConcurrentHashMap<>(); + + /** + * @param slack допустимый отступ от минимума (>= 0). + * 0 — строгое равенство (начать есть можно только тем, кто в минимуме). + * 1 — по умолчанию, мягкая справедливость. + */ + public EqualShareFairness(int slack) { + if (slack < 0) { + throw new IllegalArgumentException("slack must be >= 0"); + } + this.slack = slack; + } -final class EqualShareFairness implements FairnessStrategy { @Override public boolean tryEnterEat(int programmerId, long alreadyEaten) { - throw new UnsupportedOperationException("Not implemented"); + // Убедимся, что участник присутствует в карте (значение – завершённые порции). + finishedPerProgrammer.computeIfAbsent(programmerId, k -> new AtomicLong(0L)); + + long minFinished = currentMinFinished(); + return alreadyEaten <= minFinished + slack; } @Override public void onStartEat(int programmerId) { - throw new UnsupportedOperationException("Not implemented"); + // no-op } @Override public void onFinishEat(int programmerId) { - throw new UnsupportedOperationException("Not implemented"); + finishedPerProgrammer + .computeIfAbsent(programmerId, k -> new AtomicLong(0L)) + .incrementAndGet(); } - @Override - public Map portionsByProgrammer() { - throw new UnsupportedOperationException("Not implemented"); + private long currentMinFinished() { + long min = Long.MAX_VALUE; + boolean empty = true; + + for (AtomicLong v : finishedPerProgrammer.values()) { + long val = v.longValue(); + if (val < min) { + min = val; + } + empty = false; + } + return empty ? 0L : min; } + } \ No newline at end of file diff --git a/src/main/java/org/labs/strategy/fairness/FairnessStrategy.java b/src/main/java/org/labs/strategy/fairness/FairnessStrategy.java index e1dedbc..c8d7b28 100644 --- a/src/main/java/org/labs/strategy/fairness/FairnessStrategy.java +++ b/src/main/java/org/labs/strategy/fairness/FairnessStrategy.java @@ -1,17 +1,12 @@ package org.labs.strategy.fairness; -import java.util.Map; - public interface FairnessStrategy { - /** Разрешить ли программисту начинать есть прямо сейчас. Может блокировать или возвращать false. */ + /** Может ли программист начать есть */ boolean tryEnterEat(int programmerId, long alreadyEaten); - /** Сигнал о начале еды. */ + /** Сигнал о начале еды */ void onStartEat(int programmerId); - /** Сигнал об окончании еды. */ + /** Сигнал об окончании еды */ void onFinishEat(int programmerId); - - /** Текущая статистика порций по программистам. */ - Map portionsByProgrammer(); } \ No newline at end of file diff --git a/src/main/java/org/labs/waiter/DefaultWaiter.java b/src/main/java/org/labs/waiter/DefaultWaiter.java index 8af6746..66a6e04 100644 --- a/src/main/java/org/labs/waiter/DefaultWaiter.java +++ b/src/main/java/org/labs/waiter/DefaultWaiter.java @@ -3,38 +3,69 @@ import org.labs.core.stock.FoodStock; import org.labs.model.RefillRequest; +import java.util.Objects; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +public final class DefaultWaiter implements Waiter { + private static final long POLL_TIMEOUT_MS = 250L; -final class DefaultWaiter implements Waiter { private final int id; private final BlockingQueue queue; private final FoodStock stock; + private volatile boolean running = true; public DefaultWaiter(int id, BlockingQueue queue, FoodStock stock) { + if (id < 0) throw new IllegalArgumentException("id must be >= 0"); this.id = id; - this.queue = queue; - this.stock = stock; + this.queue = Objects.requireNonNull(queue, "queue"); + this.stock = Objects.requireNonNull(stock, "stock"); } - @Override - public int id() { return id; } - @Override public void run() { - // TODO: poll из queue, handle(request) пока running - throw new UnsupportedOperationException("Not implemented"); + try { + while (running || !queue.isEmpty()) { + try { + RefillRequest req = queue.poll(POLL_TIMEOUT_MS, TimeUnit.MILLISECONDS); + if (req != null) { + handle(req); + } + } catch (InterruptedException ie) { + if (!running) { + break; + } + } + } + } finally { + RefillRequest req; + while ((req = queue.poll()) != null) { + safeComplete(req, false); + } + } } @Override public void handle(RefillRequest request) { - // TODO: попытаться выдать 1 порцию из stock, уведомить программиста при успехе/неуспехе - throw new UnsupportedOperationException("Not implemented"); + boolean granted = stock.tryTakeOne(); + safeComplete(request, granted); } @Override public void shutdown() { - // TODO: остановка цикла + очистка - throw new UnsupportedOperationException("Not implemented"); + running = false; + } + + private static void safeComplete(RefillRequest request, boolean result) { + try { + request.tryComplete(result); + } catch (Throwable ignored) { + } + } + + @Override + public String toString() { + return "DefaultWaiter{id=" + id + ", running=" + running + '}'; } } \ No newline at end of file diff --git a/src/main/java/org/labs/waiter/Waiter.java b/src/main/java/org/labs/waiter/Waiter.java index e72ca58..b1c6379 100644 --- a/src/main/java/org/labs/waiter/Waiter.java +++ b/src/main/java/org/labs/waiter/Waiter.java @@ -4,11 +4,7 @@ import org.labs.model.RefillRequest; public interface Waiter extends Runnable { - int id(); - - /** Обрабатывает один запрос (внутри run-цикла). */ void handle(RefillRequest request); - /** Завершить работу (graceful). */ void shutdown(); } \ No newline at end of file diff --git a/src/test/java/org/labs/core/DiningTableTest.java b/src/test/java/org/labs/core/DiningTableTest.java new file mode 100644 index 0000000..4257aeb --- /dev/null +++ b/src/test/java/org/labs/core/DiningTableTest.java @@ -0,0 +1,125 @@ +package org.labs.core; + +import org.junit.jupiter.api.Test; +import org.labs.config.SimulationConfig; +import org.labs.core.actors.Programmer; +import org.labs.core.resources.LockingSpoon; +import org.labs.core.resources.Spoon; +import org.labs.core.stock.AtomicFoodStock; +import org.labs.core.stock.FoodStock; +import org.labs.metrics.SimulationStats; +import org.labs.model.RefillRequest; +import org.labs.strategy.deadlock.DeadlockAvoidanceStrategy; +import org.labs.strategy.deadlock.NMinusOneDeadlockStrategy; +import org.labs.strategy.fairness.EqualShareFairness; +import org.labs.strategy.fairness.FairnessStrategy; +import org.labs.waiter.DefaultWaiter; +import org.labs.waiter.Waiter; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +class DiningTableIntegrationTest { + + @Test + void smallSystemFinishesAndConsumesAll() throws Exception { + int N = 3; + long portions = 30; + + SimulationConfig cfg = SimulationConfig.builder() + .programmers(N).waiters(1).totalPortions(portions) + .thinkRange(Duration.ofMillis(1), Duration.ofMillis(3)) + .eatRange(Duration.ofMillis(1), Duration.ofMillis(3)) + .spoonAcquireTimeout(Duration.ofMillis(30)) + .fairShareRequired(true) + .build(); + + SimulationStats stats = new SimulationStats(); + FoodStock stock = new AtomicFoodStock(cfg.totalPortions()); + BlockingQueue q = new LinkedBlockingQueue<>(); + + DeadlockAvoidanceStrategy deadlock = new NMinusOneDeadlockStrategy(cfg.programmers()); + FairnessStrategy fairness = new EqualShareFairness(1); + + List spoons = new ArrayList<>(N); + for (int i = 0; i < N; i++) spoons.add(new LockingSpoon(i)); + + List waiters = List.of(new DefaultWaiter(0, q, stock)); + List waiterThreads = List.of(new Thread(waiters.getFirst(), "waiter-0")); + + List programmers = new ArrayList<>(N); + for (int i = 0; i < N; i++) { + Spoon left = spoons.get(i); + Spoon right = spoons.get((i + 1) % N); + programmers.add(new Programmer(i, left, right, cfg, deadlock, fairness, q, stock, stats)); + } + + DiningTable table = new DiningTable(cfg, spoons, programmers, waiters, waiterThreads, q, stock, deadlock, fairness, stats); + + table.start(); + table.awaitCompletion(); + + assertEquals(portions, stats.totalConsumed(), "All portions must be consumed"); + long sum = stats.summaryByProgrammer().values().stream().mapToLong(Long::longValue).sum(); + assertEquals(portions, sum, "Sum across programmers must equal total portions"); + + var vals = new ArrayList<>(stats.summaryByProgrammer().values()); + long max = Collections.max(vals); + long min = Collections.min(vals); + assertTrue(max - min <= 2, "Fairness should keep difference small"); + } + + @Test + void shutdownNowInterruptsAndClears() { + int N = 3; + SimulationConfig cfg = SimulationConfig.builder() + .programmers(N).waiters(1).totalPortions(10_000) // специально много + .thinkRange(Duration.ofMillis(2), Duration.ofMillis(5)) + .eatRange(Duration.ofMillis(2), Duration.ofMillis(5)) + .spoonAcquireTimeout(Duration.ofMillis(50)) + .build(); + + SimulationStats stats = new SimulationStats(); + FoodStock stock = new AtomicFoodStock(cfg.totalPortions()); + BlockingQueue q = new LinkedBlockingQueue<>(); + + DeadlockAvoidanceStrategy deadlock = new NMinusOneDeadlockStrategy(cfg.programmers()); + FairnessStrategy fairness = new EqualShareFairness(1); + + List spoons = new ArrayList<>(N); + for (int i = 0; i < N; i++) spoons.add(new LockingSpoon(i)); + + List waiters = List.of(new DefaultWaiter(0, q, stock)); + List waiterThreads = List.of(new Thread(waiters.getFirst(), "waiter-0")); + + List programmers = new ArrayList<>(N); + for (int i = 0; i < N; i++) { + programmers.add(new Programmer(i, spoons.get(i), spoons.get((i + 1) % N), cfg, deadlock, fairness, q, stock, stats)); + } + + DiningTable table = new DiningTable(cfg, spoons, programmers, waiters, waiterThreads, q, stock, deadlock, fairness, stats); + table.start(); + + try { Thread.sleep(50); } catch (InterruptedException ignored) { } + + table.shutdownNow(); + + table.waiterThreads().forEach(t -> { + try { t.join(1000); } catch (InterruptedException ignored) { } + assertFalse(t.isAlive()); + }); + table.programmerThreads().forEach(t -> { + try { t.join(1000); } catch (InterruptedException ignored) { } + assertFalse(t.isAlive()); + }); + } +} diff --git a/src/test/java/org/labs/core/actors/ProgrammerTest.java b/src/test/java/org/labs/core/actors/ProgrammerTest.java new file mode 100644 index 0000000..c931aac --- /dev/null +++ b/src/test/java/org/labs/core/actors/ProgrammerTest.java @@ -0,0 +1,78 @@ +package org.labs.core.actors; + +import org.junit.jupiter.api.Test; +import org.labs.config.SimulationConfig; +import org.labs.core.resources.LockingSpoon; +import org.labs.core.resources.Spoon; +import org.labs.core.stock.AtomicFoodStock; +import org.labs.core.stock.FoodStock; +import org.labs.metrics.SimulationStats; +import org.labs.model.RefillRequest; +import org.labs.strategy.deadlock.NMinusOneDeadlockStrategy; +import org.labs.strategy.fairness.FairnessStrategy; + +import java.time.Duration; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class ProgrammerTest { + + static final class NoopFairness implements FairnessStrategy { + @Override public boolean tryEnterEat(int programmerId, long alreadyEaten) { return true; } + @Override public void onStartEat(int programmerId) { } + @Override public void onFinishEat(int programmerId) { } + } + + @Test + void eatsExactNumberOfPortionsAndStops() throws Exception { + SimulationConfig cfg = SimulationConfig.builder() + .programmers(2).waiters(1).totalPortions(3) + .thinkRange(Duration.ofMillis(1), Duration.ofMillis(3)) + .eatRange(Duration.ofMillis(1), Duration.ofMillis(3)) + .spoonAcquireTimeout(Duration.ofMillis(20)) + .fairShareRequired(false) + .build(); + + FoodStock stock = new AtomicFoodStock(cfg.totalPortions()); + BlockingQueue queue = new LinkedBlockingQueue<>(); + SimulationStats stats = new SimulationStats(); + + Thread waiter = new Thread(() -> { + try { + while (!Thread.currentThread().isInterrupted()) { + RefillRequest r = queue.poll(200, TimeUnit.MILLISECONDS); + if (r == null) continue; + boolean ok = stock.tryTakeOne(); + r.tryComplete(ok); + if (!ok) break; + } + } catch (InterruptedException ignored) { } + }, "test-waiter"); + waiter.start(); + + Spoon left = new LockingSpoon(0); + Spoon right = new LockingSpoon(1); + + Programmer p = new Programmer( + 0, left, right, cfg, + new NMinusOneDeadlockStrategy(2), + new NoopFairness(), + queue, stock, stats + ); + + Thread pt = new Thread(p, "programmer-0"); + pt.start(); + pt.join(2000); + + waiter.interrupt(); + waiter.join(1000); + + assertFalse(pt.isAlive(), "Programmer should finish"); + assertEquals(3, p.portionsEaten()); + assertEquals(3, stats.totalConsumed()); + } +} diff --git a/src/test/java/org/labs/core/resources/LockingSpoonTest.java b/src/test/java/org/labs/core/resources/LockingSpoonTest.java new file mode 100644 index 0000000..36426d4 --- /dev/null +++ b/src/test/java/org/labs/core/resources/LockingSpoonTest.java @@ -0,0 +1,47 @@ +package org.labs.core.resources; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +class LockingSpoonTest { + + @Test + void tryAcquireAndReleaseSameThread() throws Exception { + Spoon s = new LockingSpoon(1); + assertTrue(s.tryAcquire(10, TimeUnit.MILLISECONDS)); + s.release(); + } + + @Test + void tryAcquireTimesOutWhenHeld() throws Exception { + Spoon s = new LockingSpoon(2); + Thread t = new Thread(() -> { + try { + assertTrue(s.tryAcquire(0, TimeUnit.MILLISECONDS)); + Thread.sleep(50); + s.release(); + } catch (Exception e) { throw new RuntimeException(e); } + }); + t.start(); + Thread.sleep(5); + assertFalse(s.tryAcquire(1, TimeUnit.MILLISECONDS)); + t.join(); + assertTrue(s.tryAcquire(10, TimeUnit.MILLISECONDS)); + s.release(); + } + + @Test + void releaseByNonOwnerThrows() throws Exception { + Spoon s = new LockingSpoon(3); + assertTrue(s.tryAcquire(10, TimeUnit.MILLISECONDS)); + Thread t = new Thread(() -> assertThrows(IllegalStateException.class, s::release)); + t.start(); t.join(); + s.release(); + } +} diff --git a/src/test/java/org/labs/core/stock/AtomicFoodStockTest.java b/src/test/java/org/labs/core/stock/AtomicFoodStockTest.java new file mode 100644 index 0000000..aec95ca --- /dev/null +++ b/src/test/java/org/labs/core/stock/AtomicFoodStockTest.java @@ -0,0 +1,53 @@ +package org.labs.core.stock; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +class AtomicFoodStockTest { + + @Test + void decrementsToZeroWithoutGoingNegative() { + FoodStock stock = new AtomicFoodStock(3); + assertTrue(stock.tryTakeOne()); + assertTrue(stock.tryTakeOne()); + assertTrue(stock.tryTakeOne()); + assertFalse(stock.tryTakeOne()); + assertEquals(0, stock.remaining()); + assertTrue(stock.isDepleted()); + } + + @Test + void concurrentConsumersDontLoseOrDuplicate() throws Exception { + int threads = 8; + long portions = 1000; + FoodStock stock = new AtomicFoodStock(portions); + CountDownLatch start = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(threads); + AtomicLong consumed = new AtomicLong(); + + for (int i = 0; i < threads; i++) { + new Thread(() -> { + try { + start.await(); + while (stock.tryTakeOne()) { + consumed.incrementAndGet(); + } + } catch (InterruptedException ignored) { } + done.countDown(); + }).start(); + } + start.countDown(); + done.await(); + + assertEquals(portions, consumed.get()); + assertEquals(0, stock.remaining()); + assertTrue(stock.isDepleted()); + } +} diff --git a/src/test/java/org/labs/strategy/deadlock/NMinusOneDeadlockStrategyTest.java b/src/test/java/org/labs/strategy/deadlock/NMinusOneDeadlockStrategyTest.java new file mode 100644 index 0000000..ce377ca --- /dev/null +++ b/src/test/java/org/labs/strategy/deadlock/NMinusOneDeadlockStrategyTest.java @@ -0,0 +1,44 @@ +package org.labs.strategy.deadlock; + +import org.junit.jupiter.api.Test; + + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +class NMinusOneDeadlockStrategyTest { + + @Test + void permitsAtMostNMinusOneToEnter() throws Exception { + int n = 5; + NMinusOneDeadlockStrategy s = new NMinusOneDeadlockStrategy(n, true); + + for (int i = 0; i < n - 1; i++) { + s.beforeAcquire(i); + } + assertEquals(0, s.availableSlots()); + + CountDownLatch entered = new CountDownLatch(1); + Thread t = new Thread(() -> { + try { + s.beforeAcquire(99); + entered.countDown(); + } catch (InterruptedException ignored) { } + }); + t.start(); + + Thread.sleep(20); + assertEquals(1, entered.getCount()); + + s.afterRelease(0); + + assertTrue(entered.await(200, TimeUnit.MILLISECONDS)); + + s.afterRelease(99); + for (int i = 1; i < n - 1; i++) { + s.afterRelease(i); + } + } +} diff --git a/src/test/java/org/labs/strategy/deadlock/OrderedSpoonsDeadlockStrategyTest.java b/src/test/java/org/labs/strategy/deadlock/OrderedSpoonsDeadlockStrategyTest.java new file mode 100644 index 0000000..79ac10b --- /dev/null +++ b/src/test/java/org/labs/strategy/deadlock/OrderedSpoonsDeadlockStrategyTest.java @@ -0,0 +1,32 @@ +package org.labs.strategy.deadlock; + +import org.junit.jupiter.api.Test; +import org.labs.core.resources.LockingSpoon; +import org.labs.core.resources.Spoon; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +class OrderedSpoonsDeadlockStrategyTest { + + @Test + void ordersById() { + OrderedSpoonsDeadlockStrategy st = new OrderedSpoonsDeadlockStrategy(); + Spoon a = new LockingSpoon(10); + Spoon b = new LockingSpoon(3); + + assertEquals(b, st.first(a, b)); + assertEquals(a, st.second(a, b)); + + var pair = st.order(a, b); + assertEquals(3, pair.first().id()); + assertEquals(10, pair.second().id()); + } + + @Test + void noOpsDontThrow() { + OrderedSpoonsDeadlockStrategy st = new OrderedSpoonsDeadlockStrategy(); + st.beforeAcquire(1); + st.afterRelease(1); + } +} diff --git a/src/test/java/org/labs/strategy/fairness/EqualShareFairnessTest.java b/src/test/java/org/labs/strategy/fairness/EqualShareFairnessTest.java new file mode 100644 index 0000000..d0a86c4 --- /dev/null +++ b/src/test/java/org/labs/strategy/fairness/EqualShareFairnessTest.java @@ -0,0 +1,39 @@ +package org.labs.strategy.fairness; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +class EqualShareFairnessTest { + + @Test + void slackZeroStrictEquality() { + EqualShareFairness f = new EqualShareFairness(0); + + assertTrue(f.tryEnterEat(1, 0)); + assertFalse(f.tryEnterEat(2, 1)); + + f.onFinishEat(1); + + assertFalse(f.tryEnterEat(1, 1)); + assertTrue(f.tryEnterEat(2, 0)); + } + + @Test + void slackOneAllowsLeadByOne() { + EqualShareFairness f = new EqualShareFairness(1); + + assertTrue(f.tryEnterEat(1, 0)); + assertTrue(f.tryEnterEat(2, 0)); + + f.onFinishEat(1); // 1-й завершил одну + + assertTrue(f.tryEnterEat(1, 1)); + assertTrue(f.tryEnterEat(2, 0)); + + f.onFinishEat(1); // #1 теперь 2 + assertFalse(f.tryEnterEat(1, 2)); + } +} diff --git a/src/test/java/org/labs/waiter/DefaultWaiterTest.java b/src/test/java/org/labs/waiter/DefaultWaiterTest.java new file mode 100644 index 0000000..b8bd077 --- /dev/null +++ b/src/test/java/org/labs/waiter/DefaultWaiterTest.java @@ -0,0 +1,40 @@ +package org.labs.waiter; + +import org.junit.jupiter.api.Test; +import org.labs.core.stock.AtomicFoodStock; +import org.labs.core.stock.FoodStock; +import org.labs.model.RefillRequest; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DefaultWaiterTest { + + @Test + void grantsUntilStockEmptyThenDenies() throws Exception { + FoodStock stock = new AtomicFoodStock(2); + BlockingQueue q = new LinkedBlockingQueue<>(); + DefaultWaiter w = new DefaultWaiter(1, q, stock); + Thread t = new Thread(w, "waiter-test"); + t.start(); + + RefillRequest r1 = new RefillRequest(10); + RefillRequest r2 = new RefillRequest(11); + RefillRequest r3 = new RefillRequest(12); + + q.put(r1); q.put(r2); q.put(r3); + + assertTrue(r1.resultFuture().get(1, TimeUnit.SECONDS)); + assertTrue(r2.resultFuture().get(1, TimeUnit.SECONDS)); + assertFalse(r3.resultFuture().get(1, TimeUnit.SECONDS)); + + w.shutdown(); + t.interrupt(); + t.join(1000); + assertFalse(t.isAlive()); + } +} From 24a8f1181dc9273b862088f5d8aa04fe5386c13c Mon Sep 17 00:00:00 2001 From: poma12390 <67729109+poma12390@users.noreply.github.com> Date: Tue, 23 Sep 2025 19:10:42 +0300 Subject: [PATCH 4/7] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index e974d43..ee2e76f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ Все программисты должны поесть +- одинаково, чтобы никому не было обидно - Ваша задача - реализовать симуляцию обеда с использованием языка программирования Java и многопоточности. Каждый программист должен быть представлен в виде потока, а ложки - в виде общих ресурсов, которые программисты могут захватывать и освобождать. Также не забудьте про официантов и запасы еды. @@ -29,4 +28,4 @@ * Использование системы сборки Gradle * Код должен быть отлажен и протестирован -# Дедлайн 08.10.2025 23:59 \ No newline at end of file +# Дедлайн 08.10.2025 23:59 From da5a6cde20b4c9755740d0ac0aea761ea56003b2 Mon Sep 17 00:00:00 2001 From: poma12390 Date: Thu, 25 Sep 2025 15:41:40 +0300 Subject: [PATCH 5/7] magic number fix --- src/main/java/org/labs/app/Simulation.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/labs/app/Simulation.java b/src/main/java/org/labs/app/Simulation.java index b7fbf56..0819fd2 100644 --- a/src/main/java/org/labs/app/Simulation.java +++ b/src/main/java/org/labs/app/Simulation.java @@ -36,6 +36,7 @@ public final class Simulation { null, // acquireTimeoutMs (Long) null // fairShareRequired (Boolean) ); + private static final int SLACK = 2; public static void main(String[] args) { Thread.setDefaultUncaughtExceptionHandler((t, e) -> @@ -201,7 +202,7 @@ private static DeadlockAvoidanceStrategy chooseDeadlockStrategy(SimulationConfig private static FairnessStrategy chooseFairnessStrategy(SimulationConfig cfg) { if (!cfg.fairShareRequired()) return new NoopFairness(); - return new EqualShareFairness(1); + return new EqualShareFairness(SLACK); } private static String next(String[] a, int idx, String name) { From 5d99800abb18cb8c1d0e0e3aede02021da878490 Mon Sep 17 00:00:00 2001 From: poma12390 Date: Thu, 25 Sep 2025 17:54:15 +0300 Subject: [PATCH 6/7] add virtual threds --- src/main/java/org/labs/app/Simulation.java | 10 +- src/main/java/org/labs/core/DiningTable.java | 116 ++++++++++-------- .../java/org/labs/core/DiningTableTest.java | 39 +++--- 3 files changed, 86 insertions(+), 79 deletions(-) diff --git a/src/main/java/org/labs/app/Simulation.java b/src/main/java/org/labs/app/Simulation.java index 0819fd2..5e79cb7 100644 --- a/src/main/java/org/labs/app/Simulation.java +++ b/src/main/java/org/labs/app/Simulation.java @@ -30,7 +30,7 @@ public final class Simulation { private static final ProgrammaticOverrides OVERRIDES = new ProgrammaticOverrides( 7, // programmers (Integer) e.g. 7 null, // waiters (Integer) e.g. 2 - 2000L, // totalPortions (Long) e.g. 1000 + 1000L, // totalPortions (Long) e.g. 1000 null, null, // thinkMinMs, thinkMaxMs (Long) null, null, // eatMinMs, eatMaxMs (Long) null, // acquireTimeoutMs (Long) @@ -58,12 +58,8 @@ public static void main(String[] args) { } List waiters = new ArrayList<>(cfg.waiters()); - List waiterThreads = new ArrayList<>(cfg.waiters()); for (int i = 0; i < cfg.waiters(); i++) { - Waiter w = new DefaultWaiter(i, refillQueue, stock); - Thread wt = new Thread(w, "waiter-" + i); - waiters.add(w); - waiterThreads.add(wt); + waiters.add(new DefaultWaiter(i, refillQueue, stock)); } List programmers = new ArrayList<>(n); @@ -76,7 +72,7 @@ public static void main(String[] args) { } DiningTable table = new DiningTable( - cfg, spoons, programmers, waiters, waiterThreads, + cfg, spoons, programmers, waiters, refillQueue, stock, deadlock, fairness, stats ); diff --git a/src/main/java/org/labs/core/DiningTable.java b/src/main/java/org/labs/core/DiningTable.java index 743f0d5..616f7ac 100644 --- a/src/main/java/org/labs/core/DiningTable.java +++ b/src/main/java/org/labs/core/DiningTable.java @@ -11,16 +11,20 @@ import org.labs.waiter.Waiter; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; public final class DiningTable { private final SimulationConfig cfg; private final List spoons; private final List programmers; - private final List waiterThreads; private final List waiters; private final BlockingQueue refillQueue; private final FoodStock stock; @@ -28,7 +32,11 @@ public final class DiningTable { private final FairnessStrategy fairness; private final SimulationStats stats; - private final List programmerThreads = new ArrayList<>(); + private final ExecutorService waiterPool; + private final ExecutorService programmerPool; + + private final List> waiterFutures = new ArrayList<>(); + private final List> programmerFutures = new ArrayList<>(); private volatile boolean started = false; private volatile boolean stopped = false; @@ -38,7 +46,6 @@ public DiningTable( List spoons, List programmers, List waiters, - List waiterThreads, BlockingQueue refillQueue, FoodStock stock, DeadlockAvoidanceStrategy deadlockStrategy, @@ -49,79 +56,83 @@ public DiningTable( this.spoons = List.copyOf(Objects.requireNonNull(spoons, "spoons")); this.programmers = List.copyOf(Objects.requireNonNull(programmers, "programmers")); this.waiters = List.copyOf(Objects.requireNonNull(waiters, "waiters")); - this.waiterThreads = new ArrayList<>(Objects.requireNonNull(waiterThreads, "waiterThreads")); this.refillQueue = Objects.requireNonNull(refillQueue, "refillQueue"); this.stock = Objects.requireNonNull(stock, "stock"); this.deadlockStrategy = Objects.requireNonNull(deadlockStrategy, "deadlockStrategy"); this.fairness = Objects.requireNonNull(fairness, "fairness"); this.stats = Objects.requireNonNull(stats, "stats"); - if (this.waiters.size() != this.waiterThreads.size()) { - throw new IllegalArgumentException("waiters and waiterThreads sizes must match"); - } if (this.spoons.size() < this.programmers.size()) { throw new IllegalArgumentException("spoons count must be >= programmers count"); } if (cfg.programmers() != this.programmers.size()) { throw new IllegalArgumentException("cfg.programmers must equal provided programmers size"); } + ThreadFactory waiterFactory = r -> { + Thread t = new Thread(r); + t.setName("waiter-" + t.getName()); + return t; + }; + ThreadFactory programmerFactory = r -> { + Thread t = new Thread(r); + t.setName("programmer-" + t.getName()); + return t; + }; + + this.waiterPool = Executors.newFixedThreadPool(this.waiters.size(), waiterFactory); + + this.programmerPool = Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name("programmer-", 0).factory()); } public synchronized void start() { ensureNotStopped(); - if (started) { - throw new IllegalStateException("DiningTable already started"); - } + if (started) throw new IllegalStateException("DiningTable already started"); started = true; - for (int i = 0; i < waiterThreads.size(); i++) { - Thread t = waiterThreads.get(i); - if (!t.isAlive()) { - String name = "waiter-" + i; - if (t.getName() == null || t.getName().isBlank()) { - t.setName(name); - } - t.start(); - } + for (Waiter w : waiters) { + waiterFutures.add(waiterPool.submit(w)); } - for (Programmer p : programmers) { - Thread t = new Thread(p, "programmer-" + p.id()); - programmerThreads.add(t); - t.start(); + programmerFutures.add(programmerPool.submit(p)); } } public void awaitCompletion() throws InterruptedException { ensureStarted(); - for (Thread t : programmerThreads) { - t.join(); + for (Future f : programmerFutures) { + try { + f.get(); + } catch (ExecutionException e) { + System.err.println("[DiningTable] programmer task failed: " + e.getCause()); + } } shutdownWaitersGracefully(); - for (Thread t : waiterThreads) { - t.join(); - } + waiterPool.shutdownNow(); + waiterPool.awaitTermination(5, TimeUnit.SECONDS); + + programmerPool.shutdown(); + programmerPool.awaitTermination(5, TimeUnit.SECONDS); stopped = true; } public synchronized void shutdownNow() { if (stopped) return; + for (Waiter w : waiters) { try { w.shutdown(); - } catch (Throwable ignored) {} - } - for (Thread t : programmerThreads) { - t.interrupt(); - } - for (Thread t : waiterThreads) { - t.interrupt(); + } catch (Throwable ignored) { + } } + waiterPool.shutdownNow(); + programmerPool.shutdownNow(); + refillQueue.clear(); + stopped = true; } @@ -129,26 +140,33 @@ private void shutdownWaitersGracefully() { for (Waiter w : waiters) { try { w.shutdown(); - } catch (Throwable ignored) {} - } - for (Thread t : waiterThreads) { - t.interrupt(); + } catch (Throwable ignored) { + } } } private void ensureStarted() { - if (!started) { - throw new IllegalStateException("DiningTable not started"); - } + if (!started) throw new IllegalStateException("DiningTable not started"); } private void ensureNotStopped() { - if (stopped) { - throw new IllegalStateException("DiningTable already stopped"); - } + if (stopped) throw new IllegalStateException("DiningTable already stopped"); } - public List programmerThreads() { return Collections.unmodifiableList(programmerThreads); } - public List waiterThreads() { return Collections.unmodifiableList(waiterThreads); } -} \ No newline at end of file + public List> programmerTasks() { + return List.copyOf(programmerFutures); + } + + public List> waiterTasks() { + return List.copyOf(waiterFutures); + } + + public ExecutorService programmerExecutor() { + return programmerPool; + } + + public ExecutorService waiterExecutor() { + return waiterPool; + } +} diff --git a/src/test/java/org/labs/core/DiningTableTest.java b/src/test/java/org/labs/core/DiningTableTest.java index 4257aeb..0c51cdb 100644 --- a/src/test/java/org/labs/core/DiningTableTest.java +++ b/src/test/java/org/labs/core/DiningTableTest.java @@ -17,16 +17,10 @@ import org.labs.waiter.Waiter; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.*; +import java.util.concurrent.*; +import static org.junit.jupiter.api.Assertions.*; class DiningTableIntegrationTest { @@ -54,7 +48,6 @@ void smallSystemFinishesAndConsumesAll() throws Exception { for (int i = 0; i < N; i++) spoons.add(new LockingSpoon(i)); List waiters = List.of(new DefaultWaiter(0, q, stock)); - List waiterThreads = List.of(new Thread(waiters.getFirst(), "waiter-0")); List programmers = new ArrayList<>(N); for (int i = 0; i < N; i++) { @@ -63,7 +56,7 @@ void smallSystemFinishesAndConsumesAll() throws Exception { programmers.add(new Programmer(i, left, right, cfg, deadlock, fairness, q, stock, stats)); } - DiningTable table = new DiningTable(cfg, spoons, programmers, waiters, waiterThreads, q, stock, deadlock, fairness, stats); + DiningTable table = new DiningTable(cfg, spoons, programmers, waiters, q, stock, deadlock, fairness, stats); table.start(); table.awaitCompletion(); @@ -79,7 +72,7 @@ void smallSystemFinishesAndConsumesAll() throws Exception { } @Test - void shutdownNowInterruptsAndClears() { + void shutdownNowInterruptsAndClears() throws Exception { int N = 3; SimulationConfig cfg = SimulationConfig.builder() .programmers(N).waiters(1).totalPortions(10_000) // специально много @@ -99,27 +92,27 @@ void shutdownNowInterruptsAndClears() { for (int i = 0; i < N; i++) spoons.add(new LockingSpoon(i)); List waiters = List.of(new DefaultWaiter(0, q, stock)); - List waiterThreads = List.of(new Thread(waiters.getFirst(), "waiter-0")); List programmers = new ArrayList<>(N); for (int i = 0; i < N; i++) { programmers.add(new Programmer(i, spoons.get(i), spoons.get((i + 1) % N), cfg, deadlock, fairness, q, stock, stats)); } - DiningTable table = new DiningTable(cfg, spoons, programmers, waiters, waiterThreads, q, stock, deadlock, fairness, stats); + DiningTable table = new DiningTable(cfg, spoons, programmers, waiters, q, stock, deadlock, fairness, stats); table.start(); + // Немного поработаем try { Thread.sleep(50); } catch (InterruptedException ignored) { } + // Аварийно остановим table.shutdownNow(); - table.waiterThreads().forEach(t -> { - try { t.join(1000); } catch (InterruptedException ignored) { } - assertFalse(t.isAlive()); - }); - table.programmerThreads().forEach(t -> { - try { t.join(1000); } catch (InterruptedException ignored) { } - assertFalse(t.isAlive()); - }); + // Ждём завершение пулов + assertTrue(table.programmerExecutor().awaitTermination(2, TimeUnit.SECONDS), "Programmer pool did not terminate"); + assertTrue(table.waiterExecutor().awaitTermination(2, TimeUnit.SECONDS), "Waiter pool did not terminate"); + + // Все задачи должны быть завершены/отменены + table.programmerTasks().forEach(f -> assertTrue(f.isDone() || f.isCancelled())); + table.waiterTasks().forEach(f -> assertTrue(f.isDone() || f.isCancelled())); } -} +} \ No newline at end of file From 485677b3d944ca8efbeaa17ec2bb3d02f2027a67 Mon Sep 17 00:00:00 2001 From: poma12390 Date: Tue, 30 Sep 2025 16:58:38 +0300 Subject: [PATCH 7/7] fix queue --- src/main/java/org/labs/app/Simulation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/labs/app/Simulation.java b/src/main/java/org/labs/app/Simulation.java index 5e79cb7..d52b213 100644 --- a/src/main/java/org/labs/app/Simulation.java +++ b/src/main/java/org/labs/app/Simulation.java @@ -30,7 +30,7 @@ public final class Simulation { private static final ProgrammaticOverrides OVERRIDES = new ProgrammaticOverrides( 7, // programmers (Integer) e.g. 7 null, // waiters (Integer) e.g. 2 - 1000L, // totalPortions (Long) e.g. 1000 + 5000L, // totalPortions (Long) e.g. 1000 null, null, // thinkMinMs, thinkMaxMs (Long) null, null, // eatMinMs, eatMaxMs (Long) null, // acquireTimeoutMs (Long)