diff --git a/.gitignore b/.gitignore index b63da45..d9c5ded 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +### Log files ### +*.log \ No newline at end of file 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 # Цели и задачи л/р: diff --git a/src/main/java/org/labs/App.java b/src/main/java/org/labs/App.java new file mode 100644 index 0000000..672e514 --- /dev/null +++ b/src/main/java/org/labs/App.java @@ -0,0 +1,21 @@ +package org.labs; + +import org.labs.restaurant.Restaurant; + +public class App { + public static void main(String[] args) { + var restaurant = new Restaurant(Constants.PROGRAMMERS_COUNT, Constants.WAITERS_COUNT, Constants.PORTIONS_COUNT); + restaurant.startDinner(); + var dinnerInfo = restaurant.getDinnerInfo(); + + for (var programmerInfo : dinnerInfo.programmersEatenPortions().entrySet()) { + System.out.println(new StringBuilder().append("Программист ").append(programmerInfo.getKey()) + .append(" съел ").append(programmerInfo.getValue()).append(" порций").toString()); + } + + for (var portion : dinnerInfo.waitersServedPortions().entrySet()) { + System.out.println(new StringBuilder().append("Официант ").append(portion.getKey()) + .append(" разнес ").append(portion.getValue()).append(" порций").toString()); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/labs/Constants.java b/src/main/java/org/labs/Constants.java new file mode 100644 index 0000000..239257a --- /dev/null +++ b/src/main/java/org/labs/Constants.java @@ -0,0 +1,10 @@ +package org.labs; + +public class Constants { + public static final Integer MAX_EATING_TIME_MS = 100; + public static final Integer MAX_SERVER_TIME_MS = 100; + + public static final Integer PORTIONS_COUNT = 1_000_000; + public static final Integer PROGRAMMERS_COUNT = 7; + public static final Integer WAITERS_COUNT = 2; +} diff --git a/src/main/java/org/labs/Main.java b/src/main/java/org/labs/Main.java deleted file mode 100644 index 9917247..0000000 --- a/src/main/java/org/labs/Main.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.labs; - -public class Main { - public static void main(String[] args) { - System.out.println("Hello, World!"); - } -} \ No newline at end of file diff --git a/src/main/java/org/labs/logger/ConsoleLogger.java b/src/main/java/org/labs/logger/ConsoleLogger.java new file mode 100644 index 0000000..0c88bc5 --- /dev/null +++ b/src/main/java/org/labs/logger/ConsoleLogger.java @@ -0,0 +1,19 @@ +package org.labs.logger; + +public class ConsoleLogger implements Logger { + private String name; + + public ConsoleLogger() { + this(""); + } + + public ConsoleLogger(String name) { + this.name = name; + } + + @Override + public void log(String... messages) { + final var prefix = !name.isEmpty() ? name + " " : ""; + System.out.println(prefix + String.join(" ", messages)); + } +} diff --git a/src/main/java/org/labs/logger/Logger.java b/src/main/java/org/labs/logger/Logger.java new file mode 100644 index 0000000..9802445 --- /dev/null +++ b/src/main/java/org/labs/logger/Logger.java @@ -0,0 +1,5 @@ +package org.labs.logger; + +public interface Logger { + void log(String... messages); +} diff --git a/src/main/java/org/labs/programmer/Programmer.java b/src/main/java/org/labs/programmer/Programmer.java new file mode 100644 index 0000000..7a5089f --- /dev/null +++ b/src/main/java/org/labs/programmer/Programmer.java @@ -0,0 +1,76 @@ +package org.labs.programmer; + +import java.util.Random; + +import org.labs.Constants; +import org.labs.logger.ConsoleLogger; +import org.labs.logger.Logger; +import org.labs.spoon.Spoon; +import org.labs.waiter.WaiterService; + +public class Programmer extends Thread { + private final Integer id; + private final Logger logger; + private final Random random = new Random(); + + private Integer eatenPortions = 0; + + private final WaiterService waiters; + private final Spoon firstSpoon; + private final Spoon secondSpoon; + + public Programmer(Integer id, WaiterService waiters, Spoon leftSpoon, Spoon rightSpoon) { + super("Программист " + id); + + this.id = id; + this.waiters = waiters; + + if (leftSpoon.getId() < rightSpoon.getId()) { + this.firstSpoon = leftSpoon; + this.secondSpoon = rightSpoon; + } else { + this.firstSpoon = rightSpoon; + this.secondSpoon = leftSpoon; + } + + this.logger = new ConsoleLogger(getName()); + } + + @Override + public void run() { + try { + logger.log("начал есть"); + while (true) { + + logger.log("просит новую порцию"); + try { + if (!waiters.getPortion(getName())) { + break; + } + } catch (Exception e) { + break; + } + + firstSpoon.take(getName()); + secondSpoon.take(getName()); + + Thread.sleep(random.nextInt(Constants.MAX_EATING_TIME_MS)); + eatenPortions++; + logger.log("доел порцию"); + + secondSpoon.put(getName()); + firstSpoon.put(getName()); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public Integer getProgrammerId() { + return id; + } + + public Integer getEatenPortions() { + return eatenPortions; + } +} diff --git a/src/main/java/org/labs/restaurant/DinnerInfo.java b/src/main/java/org/labs/restaurant/DinnerInfo.java new file mode 100644 index 0000000..adc9ff4 --- /dev/null +++ b/src/main/java/org/labs/restaurant/DinnerInfo.java @@ -0,0 +1,6 @@ +package org.labs.restaurant; + +import java.util.Map; + +public record DinnerInfo(Map programmersEatenPortions, Map waitersServedPortions) { +} diff --git a/src/main/java/org/labs/restaurant/Restaurant.java b/src/main/java/org/labs/restaurant/Restaurant.java new file mode 100644 index 0000000..3f17116 --- /dev/null +++ b/src/main/java/org/labs/restaurant/Restaurant.java @@ -0,0 +1,61 @@ +package org.labs.restaurant; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.labs.logger.ConsoleLogger; +import org.labs.logger.Logger; +import org.labs.programmer.Programmer; +import org.labs.spoon.Spoon; +import org.labs.waiter.WaiterService; + +public class Restaurant { + private final Logger logger; + + private final WaiterService waiters; + private final List spoons = new ArrayList<>(); + private final List programmers = new ArrayList<>(); + + public Restaurant(Integer programmresCount, Integer waitersCount, Integer portionsCount) { + logger = new ConsoleLogger("Ресторан"); + + waiters = new WaiterService(waitersCount, portionsCount); + + for (Integer i = 0; i < programmresCount; i++) { + spoons.add(new Spoon(i)); + } + + for (Integer i = 0; i < programmresCount; i++) { + Integer leftSpoonIdx = i; + Integer rightSpoonIdx = (i + 1) % spoons.size(); + + programmers.add(new Programmer(i, waiters, spoons.get(leftSpoonIdx), spoons.get(rightSpoonIdx))); + } + } + + public void startDinner() { + for (var p : programmers) { + p.start(); + } + + logger.log("начал ужин"); + + for (var p : programmers) { + try { + p.join(); + } catch (InterruptedException e) { + } + } + + logger.log("закончил ужин"); + } + + public DinnerInfo getDinnerInfo() { + Map programmersEatenPortions = programmers.stream() + .collect(Collectors.toMap(Programmer::getProgrammerId, Programmer::getEatenPortions)); + Map waitersServedPortions = waiters.getServedPortions(); + return new DinnerInfo(programmersEatenPortions, waitersServedPortions); + } +} diff --git a/src/main/java/org/labs/spoon/Spoon.java b/src/main/java/org/labs/spoon/Spoon.java new file mode 100644 index 0000000..a788c64 --- /dev/null +++ b/src/main/java/org/labs/spoon/Spoon.java @@ -0,0 +1,33 @@ +package org.labs.spoon; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.labs.logger.ConsoleLogger; +import org.labs.logger.Logger; + +public class Spoon { + private final Integer id; + private final Logger logger; + private Lock lock = new ReentrantLock(); + + public Spoon(Integer id) { + this.id = id; + this.logger = new ConsoleLogger("Ложка " + id); + } + + public void take(String actor) { + logger.log(actor, "пытается взять ложку"); + lock.lock(); + logger.log(actor, "взял ложку"); + } + + public void put(String actor) { + lock.unlock(); + logger.log(actor, "положил ложку"); + } + + public Integer getId() { + return id; + } +} 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..213c310 --- /dev/null +++ b/src/main/java/org/labs/waiter/Waiter.java @@ -0,0 +1,35 @@ +package org.labs.waiter; + +import java.util.Random; + +import org.labs.Constants; +import org.labs.logger.ConsoleLogger; +import org.labs.logger.Logger; + +class Waiter { + private final Integer id; + + private final Logger logger; + private final Random random = new Random(); + + private Integer serverPortions = 0; + + public Waiter(Integer id) { + this.id = id; + this.logger = new ConsoleLogger("Официант " + id); + } + + public synchronized void serverPortion(String client) throws Exception { + logger.log("понес порцию", client); + Thread.sleep(random.nextInt(Constants.MAX_SERVER_TIME_MS)); + serverPortions++; + } + + public Integer getId() { + return id; + } + + public Integer getServedPortions() { + return serverPortions; + } +} diff --git a/src/main/java/org/labs/waiter/WaiterService.java b/src/main/java/org/labs/waiter/WaiterService.java new file mode 100644 index 0000000..9619699 --- /dev/null +++ b/src/main/java/org/labs/waiter/WaiterService.java @@ -0,0 +1,51 @@ +package org.labs.waiter; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import org.labs.logger.ConsoleLogger; +import org.labs.logger.Logger; + +public class WaiterService { + private final Logger logger; + + private final AtomicInteger portionsCount; + + private final List waiters; + private final AtomicInteger nextWaiter; + + public WaiterService(Integer waitersCount, Integer portionsCount) { + Waiter[] mutableWaiters = new Waiter[waitersCount]; + for (Integer i = 0; i < waitersCount; i++) { + mutableWaiters[i] = new Waiter(i); + } + waiters = List.of(mutableWaiters); + nextWaiter = new AtomicInteger(0); + this.portionsCount = new AtomicInteger(portionsCount); + logger = new ConsoleLogger("Сервис официантов"); + } + + public Boolean getPortion(String client) throws Exception { + logger.log(client, "просит порцию"); + + var idx = nextWaiter.getAndUpdate((val) -> (val + 1) % waiters.size()); + var waiter = waiters.get(idx); + + Integer remainingPortions = portionsCount.decrementAndGet(); + if (remainingPortions < 0) { + logger.log(client, "узнал, что еда закончилась"); + return false; + } + + waiter.serverPortion(client); + logger.log(client, "получил порцию"); + logger.log("порций осталось", remainingPortions.toString()); + return true; + } + + public Map getServedPortions() { + return waiters.stream().collect(Collectors.toMap(Waiter::getId, Waiter::getServedPortions)); + } +} diff --git a/src/test/java/org/labs/TestConstants.java b/src/test/java/org/labs/TestConstants.java new file mode 100644 index 0000000..1e03253 --- /dev/null +++ b/src/test/java/org/labs/TestConstants.java @@ -0,0 +1,13 @@ +package org.labs; + +public class TestConstants { + public static final Integer MAX_EATING_TIME_MS = 1; + public static final Integer MAX_SERVER_TIME_MS = 1; + + public static final Integer PORTIONS_COUNT = 100; + public static final Integer PROGRAMMERS_COUNT = 7; + public static final Integer WAITERS_COUNT = 2; + + public static final Float AVERAGE_DELTA_PERCENT = 0.1f; + public static final Float DIFF_PERCENT = 0.25f; +} diff --git a/src/test/java/org/labs/restaurant/RestaurantTest.java b/src/test/java/org/labs/restaurant/RestaurantTest.java new file mode 100644 index 0000000..39dced2 --- /dev/null +++ b/src/test/java/org/labs/restaurant/RestaurantTest.java @@ -0,0 +1,113 @@ +package org.labs.restaurant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map.Entry; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; +import org.labs.TestConstants; + +public class RestaurantTest { + private Restaurant restaurant; + private DinnerInfo dinnerInfo; + + @BeforeEach + void setup() { + restaurant = new Restaurant(TestConstants.PROGRAMMERS_COUNT, TestConstants.WAITERS_COUNT, + TestConstants.PORTIONS_COUNT); + restaurant.startDinner(); + dinnerInfo = restaurant.getDinnerInfo(); + } + + @RepeatedTest(value = 5) + void getDinnerInfo_waitersServedAllPortions() { + var serverPortions = dinnerInfo.waitersServedPortions().entrySet().stream().map(Entry::getValue) + .reduce(0, Integer::sum); + + assertEquals(TestConstants.PORTIONS_COUNT, serverPortions, "Официанты разнесли все порции"); + } + + @RepeatedTest(value = 5) + void getDinnerInfo_waitersServedPortionsAverageFairness() { + var expectedAverage = TestConstants.PORTIONS_COUNT.floatValue() / TestConstants.WAITERS_COUNT; + var actualAverage = dinnerInfo.waitersServedPortions().entrySet().stream().mapToInt(Entry::getValue) + .average(); + + assertTrue(actualAverage.isPresent()); + assertEquals(expectedAverage, actualAverage.getAsDouble(), + TestConstants.AVERAGE_DELTA_PERCENT * expectedAverage, + "Среднее кол-во разнесенных официантом порций не отклоняется от ожидаемого более чем на " + + TestConstants.AVERAGE_DELTA_PERCENT * 100 + "%"); + } + + @RepeatedTest(value = 5) + void getDinnerInfo_waitersServedPortionsMaxFairness() { + var expectedAverage = TestConstants.PORTIONS_COUNT / TestConstants.WAITERS_COUNT; + var actualMax = dinnerInfo.waitersServedPortions().entrySet().stream().mapToInt(Entry::getValue) + .max(); + + assertTrue(actualMax.isPresent()); + assertTrue(Math.abs(expectedAverage - actualMax.getAsInt()) < TestConstants.DIFF_PERCENT * expectedAverage, + "Максимальное кол-во разнесенных официантом порций не отклоняется от ожидаемого более чем на " + + TestConstants.DIFF_PERCENT * 100 + "%"); + } + + @RepeatedTest(value = 5) + void getDinnerInfo_waitersServedPortionsMinFairness() { + var expectedAverage = TestConstants.PORTIONS_COUNT / TestConstants.WAITERS_COUNT; + var actualMin = dinnerInfo.waitersServedPortions().entrySet().stream().mapToInt(Entry::getValue) + .min(); + + assertTrue(actualMin.isPresent()); + assertTrue(Math.abs(expectedAverage - actualMin.getAsInt()) < TestConstants.DIFF_PERCENT * expectedAverage, + "Минимальное кол-во разнесенных официантом порций не отклоняется от ожидаемого более чем на " + + TestConstants.DIFF_PERCENT * 100 + "%"); + } + + @RepeatedTest(value = 5) + void getDinnerInfo_programmersEatenAllPortions() { + var eatenPortions = dinnerInfo.programmersEatenPortions().entrySet().stream().mapToInt(Entry::getValue) + .sum(); + + assertEquals(TestConstants.PORTIONS_COUNT, eatenPortions, "Программисты съели все порции"); + } + + @RepeatedTest(value = 5) + void getDinnerInfo_programmersEatenPortionAverageFairness() { + var expectedAverage = TestConstants.PORTIONS_COUNT.floatValue() / TestConstants.PROGRAMMERS_COUNT; + var actualAverage = dinnerInfo.programmersEatenPortions().entrySet().stream().mapToInt(Entry::getValue) + .average(); + + assertTrue(actualAverage.isPresent()); + assertEquals(expectedAverage, actualAverage.getAsDouble(), + TestConstants.AVERAGE_DELTA_PERCENT * expectedAverage, + "Среднее кол-во съеденных порций не отклоняется от ожидаемого более чем на " + + TestConstants.AVERAGE_DELTA_PERCENT * 100 + "%"); + } + + @RepeatedTest(value = 5) + void getDinnerInfo_programmersEatenPortionMaxFairness() { + var expectedAverage = TestConstants.PORTIONS_COUNT / TestConstants.PROGRAMMERS_COUNT; + var actualMax = dinnerInfo.programmersEatenPortions().entrySet().stream().mapToInt(Entry::getValue) + .max(); + + assertTrue(actualMax.isPresent()); + assertTrue(Math.abs(expectedAverage - actualMax.getAsInt()) < TestConstants.DIFF_PERCENT * expectedAverage, + "Максимальное кол-во съеденных программистом порций не отклоняется от ожидаемого более чем на " + + TestConstants.DIFF_PERCENT * 100 + "%"); + } + + @RepeatedTest(value = 5) + void getDinnerInfo_programmersEatenPortionMinFairness() { + var expectedAverage = TestConstants.PORTIONS_COUNT / TestConstants.PROGRAMMERS_COUNT; + var actualMin = dinnerInfo.programmersEatenPortions().entrySet().stream().mapToInt(Entry::getValue) + .min(); + + assertTrue(actualMin.isPresent()); + assertTrue(Math.abs(expectedAverage - actualMin.getAsInt()) < TestConstants.DIFF_PERCENT * expectedAverage, + "Минимальное кол-во съеденных программистом порций не отклоняется от ожидаемого более чем на " + + TestConstants.DIFF_PERCENT * 100 + "%"); + } +}