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/Main.java b/src/main/java/org/labs/Main.java index 9917247..98a18f8 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -1,7 +1,16 @@ package org.labs; +import org.labs.dining.DiningProgrammersWorld; + +import java.util.Arrays; + public class Main { + public static void main(String[] args) { - System.out.println("Hello, World!"); + try { + new DiningProgrammersWorld().startDining(1_000, 3, 7); + } catch (InterruptedException e) { + System.out.println("Ooopps... " + Arrays.toString(e.getStackTrace())); + } } } \ No newline at end of file diff --git a/src/main/java/org/labs/dining/DiningProgrammersWorld.java b/src/main/java/org/labs/dining/DiningProgrammersWorld.java new file mode 100644 index 0000000..ed22e60 --- /dev/null +++ b/src/main/java/org/labs/dining/DiningProgrammersWorld.java @@ -0,0 +1,46 @@ +package org.labs.dining; + +import org.labs.dining.items.Programmer; +import org.labs.dining.items.Spoon; + +public class DiningProgrammersWorld { + + private static final int DEFAULT_PORTIONS_AMOUNT = 1_000_000; + private static final int DEFAULT_WAITERS_AMOUNT = 2; + private static final int DEFAULT_PROGRAMMERS_AMOUNT = 5; + + private Programmer[] programmers; + + public Programmer[] getProgrammers() { + return programmers; + } + + public void startDining( + Integer portionsAmount, + Integer waitersAmount, + Integer programmersAmount + ) throws InterruptedException { + int portionsAmountToInitialize = portionsAmount == null ? DEFAULT_PORTIONS_AMOUNT : portionsAmount; + int waitersAmountToInitialize = waitersAmount == null ? DEFAULT_WAITERS_AMOUNT : waitersAmount; + int programmersAmountToInitialize = programmersAmount == null ? DEFAULT_PROGRAMMERS_AMOUNT : programmersAmount; + + SharedContext.initialize(portionsAmountToInitialize, waitersAmountToInitialize, programmersAmountToInitialize); + programmers = new Programmer[programmersAmountToInitialize]; + + for (int i = 0; i < programmersAmountToInitialize; i++) { + Spoon leftSpoon = SharedContext.getSpoon(i); + Spoon rightSpoon = SharedContext.getSpoon((i + 1) % programmersAmountToInitialize); + + String programmerName = "Programmer_" + i; + Programmer currentProgrammer = i == programmersAmountToInitialize - 1 + ? new Programmer(programmerName, rightSpoon, leftSpoon) + : new Programmer(programmerName, leftSpoon, rightSpoon); + currentProgrammer.start(); + programmers[i] = currentProgrammer; + } + + for (int i = 0; i < programmersAmountToInitialize; i++) { + programmers[i].join(); + } + } +} diff --git a/src/main/java/org/labs/dining/SharedContext.java b/src/main/java/org/labs/dining/SharedContext.java new file mode 100644 index 0000000..5c39084 --- /dev/null +++ b/src/main/java/org/labs/dining/SharedContext.java @@ -0,0 +1,42 @@ +package org.labs.dining; + +import org.labs.dining.items.Spoon; +import org.labs.dining.items.Waiter; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +public class SharedContext { + + private static Waiter[] waiters; + private static Spoon[] spoons; + private static BlockingQueue waiterBlockingQueue; + + public static Spoon getSpoon(int atIndex) { + return spoons[atIndex]; + } + + public static BlockingQueue getWaiterBlockingQueue() { + return waiterBlockingQueue; + } + + public static void initialize( + int portionsAmount, + int waitersAmount, + int spoonsAmount + ) throws InterruptedException { + Waiter.setPortionsAmount(portionsAmount); + + SharedContext.waiters = new Waiter[waitersAmount]; + SharedContext.spoons = new Spoon[spoonsAmount]; + SharedContext.waiterBlockingQueue = new ArrayBlockingQueue<>(waitersAmount); + + for (int i = 0; i < waitersAmount; i++) { + SharedContext.waiters[i] = new Waiter(); + SharedContext.waiterBlockingQueue.put(SharedContext.waiters[i]); + } + for (int i = 0; i < spoonsAmount; i++) { + SharedContext.spoons[i] = new Spoon(); + } + } +} diff --git a/src/main/java/org/labs/dining/items/Programmer.java b/src/main/java/org/labs/dining/items/Programmer.java new file mode 100644 index 0000000..cb61b41 --- /dev/null +++ b/src/main/java/org/labs/dining/items/Programmer.java @@ -0,0 +1,79 @@ +package org.labs.dining.items; + +import org.labs.dining.SharedContext; + +public class Programmer extends Thread { + + private final Spoon leftSpoon; + private final Spoon rightSpoon; + + private int portionsConsumed = 0; + + public Programmer(String programmerName, Spoon leftSpoon, Spoon rightSpoon) { + super(programmerName); + this.leftSpoon = leftSpoon; + this.rightSpoon = rightSpoon; + } + + public int getPortionsConsumed() { + return portionsConsumed; + } + + /** + * Любое действие выводится в консоль, поэтому управление "длительностью" действия + * происходит здесь - в одном месте + *

+ * Выбирается случайное время для задержки, печатается действия и поток "засыпает" на выбранное время + */ + private synchronized void printMessage(String message) throws InterruptedException { + int ms = (int) (Math.random() * 100); + System.out.println(Thread.currentThread().getName() + " " + message + " for: " + ms + "ms"); + Thread.sleep(ms); + } + + public void trashTalk() throws InterruptedException { + printMessage("is trash-talking"); + } + + public boolean requestPortion() throws InterruptedException { + Waiter waiter = SharedContext.getWaiterBlockingQueue().take(); + boolean portionTaken = waiter.tryTakePortion(); + SharedContext.getWaiterBlockingQueue().put(waiter); + return portionTaken; + } + + public void takeSpoonsAndEat() throws InterruptedException { + synchronized (leftSpoon) { + printMessage("takes left spoon"); + synchronized (rightSpoon) { + printMessage("takes right spoon"); + eat(); + } + } + } + + private void eat() throws InterruptedException { + printMessage("is eating"); + this.portionsConsumed++; + } + + public void putSpoons() throws InterruptedException { + printMessage("releasing spoons"); + } + + @Override + public void run() { + try { + while (true) { + trashTalk(); + boolean portionTaken = requestPortion(); + if (!portionTaken) break; + takeSpoonsAndEat(); + putSpoons(); + } + System.out.println("Done " + this.getName() + " with consumed: " + this.portionsConsumed); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/org/labs/dining/items/Spoon.java b/src/main/java/org/labs/dining/items/Spoon.java new file mode 100644 index 0000000..3a52719 --- /dev/null +++ b/src/main/java/org/labs/dining/items/Spoon.java @@ -0,0 +1,4 @@ +package org.labs.dining.items; + +public class Spoon { +} diff --git a/src/main/java/org/labs/dining/items/Waiter.java b/src/main/java/org/labs/dining/items/Waiter.java new file mode 100644 index 0000000..ba076a4 --- /dev/null +++ b/src/main/java/org/labs/dining/items/Waiter.java @@ -0,0 +1,17 @@ +package org.labs.dining.items; + +import java.util.concurrent.atomic.AtomicInteger; + +public class Waiter { + + private static AtomicInteger portionsAmount; + + public static void setPortionsAmount(int portionsAmount) { + Waiter.portionsAmount = new AtomicInteger(portionsAmount); + } + + public boolean tryTakePortion() { + int decrementedPortionsAmount = portionsAmount.getAndDecrement(); + return decrementedPortionsAmount > 0; + } +} diff --git a/src/test/java/dining/DiningProgrammersWorldTest.java b/src/test/java/dining/DiningProgrammersWorldTest.java new file mode 100644 index 0000000..0ea9278 --- /dev/null +++ b/src/test/java/dining/DiningProgrammersWorldTest.java @@ -0,0 +1,78 @@ +package dining; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.labs.dining.DiningProgrammersWorld; +import org.labs.dining.items.Programmer; + +import java.util.Arrays; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DiningProgrammersWorldTest { + + private static final int SMALL_PORTIONS_AMOUNT_TO_SIMULATE = 1000; + private static final int MEDIUM_PORTIONS_AMOUNT_TO_SIMULATE = 10_000; + private static final int BIG_PORTIONS_AMOUNT_TO_SIMULATE = 100_000; + private static final int LARGE_PORTIONS_AMOUNT_TO_SIMULATE = 1_000_000; + + private static final int WAITERS_AMOUNT_TO_SIMULATE = 2; + + private static final int PROGRAMMERS_AMOUNT_TO_SIMULATE = 5; + + public record DiningProgrammersWorldArgument(int portionsAmount, int waitersAmount, int programmersAmount){} + + private static Stream provideArguments() { + return Stream.of( + Arguments.of(new DiningProgrammersWorldArgument( + SMALL_PORTIONS_AMOUNT_TO_SIMULATE, + WAITERS_AMOUNT_TO_SIMULATE, + PROGRAMMERS_AMOUNT_TO_SIMULATE + ) + ), + Arguments.of(new DiningProgrammersWorldArgument( + MEDIUM_PORTIONS_AMOUNT_TO_SIMULATE, + WAITERS_AMOUNT_TO_SIMULATE, + PROGRAMMERS_AMOUNT_TO_SIMULATE + ) + ), + Arguments.of(new DiningProgrammersWorldArgument( + BIG_PORTIONS_AMOUNT_TO_SIMULATE, + WAITERS_AMOUNT_TO_SIMULATE, + PROGRAMMERS_AMOUNT_TO_SIMULATE + ) + ) + ); + } + + @Test + public void testNoDeadlocks() throws InterruptedException { + new DiningProgrammersWorld().startDining( + LARGE_PORTIONS_AMOUNT_TO_SIMULATE, + WAITERS_AMOUNT_TO_SIMULATE, + PROGRAMMERS_AMOUNT_TO_SIMULATE + ); + } + + @ParameterizedTest + @MethodSource("provideArguments") + public void testNoRaceCondition(DiningProgrammersWorldArgument argument) throws InterruptedException { + DiningProgrammersWorld diningProgrammersWorld = new DiningProgrammersWorld(); + diningProgrammersWorld.startDining( + argument.portionsAmount(), + argument.waitersAmount(), + argument.programmersAmount() + ); + + Programmer[] programmers = diningProgrammersWorld.getProgrammers(); + + int consumedPortionsSum = Arrays.stream(programmers) + .map(Programmer::getPortionsConsumed) + .reduce(0, Integer::sum); + + assertEquals(argument.portionsAmount(), consumedPortionsSum); + } +}