From 98bbc40513f90f1e836ec1897656331de6f2b3c3 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Sun, 21 Sep 2025 17:34:58 +0000 Subject: [PATCH 1/4] 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 f9435137307021150dd51b8ee134ca5afd783205 Mon Sep 17 00:00:00 2001 From: Maria Barkovskaya Date: Mon, 29 Sep 2025 01:46:46 +0300 Subject: [PATCH 2/4] implement homework --- .gitignore | 1 + build.gradle.kts | 5 + src/main/java/org/labs/Main.java | 65 ++++++++++- .../java/org/labs/lunch/DinningTable.java | 52 +++++++++ src/main/java/org/labs/lunch/Programmer.java | 84 +++++++++++++++ src/main/java/org/labs/lunch/Restaurant.java | 102 ++++++++++++++++++ src/main/java/org/labs/lunch/Spoon.java | 21 ++++ src/main/java/org/labs/lunch/Waiter.java | 56 ++++++++++ src/main/resources/logback.xml | 12 +++ .../java/org/labs/lunch/FairnessTest.java | 44 ++++++++ .../java/org/labs/lunch/RestaurantTest.java | 45 ++++++++ 11 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/labs/lunch/DinningTable.java create mode 100644 src/main/java/org/labs/lunch/Programmer.java create mode 100644 src/main/java/org/labs/lunch/Restaurant.java create mode 100644 src/main/java/org/labs/lunch/Spoon.java create mode 100644 src/main/java/org/labs/lunch/Waiter.java create mode 100644 src/main/resources/logback.xml create mode 100644 src/test/java/org/labs/lunch/FairnessTest.java create mode 100644 src/test/java/org/labs/lunch/RestaurantTest.java diff --git a/.gitignore b/.gitignore index b63da45..df54656 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/build.gradle.kts b/build.gradle.kts index bda0d97..bcbfd66 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,11 @@ repositories { dependencies { testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.openjdk.jmh:jmh-core:1.36") + + implementation("commons-cli:commons-cli:1.5.0") + implementation("org.slf4j:slf4j-api:2.0.10") + implementation("ch.qos.logback:logback-classic:1.5.13") } tasks.test { diff --git a/src/main/java/org/labs/Main.java b/src/main/java/org/labs/Main.java index 9917247..19c55a9 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -1,7 +1,70 @@ package org.labs; +import org.apache.commons.cli.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.labs.lunch.Restaurant; + +import java.util.concurrent.TimeUnit; + public class Main { public static void main(String[] args) { - System.out.println("Hello, World!"); + final Logger logger = LoggerFactory.getLogger(Main.class); + + int programmersCount = 7; + int waitersCount = 2; + int portionsCount = 5_000; + int timeout = 60; + + Options options = new Options(); + options.addOption("p", "programmers", true, "Number of programmers"); + options.addOption("w", "waiters", true, "Number of waiters"); + options.addOption("f", "food", true, "Total portions count"); + options.addOption("t", "timeout", true, "Timeout in seconds"); + + CommandLineParser parser = new DefaultParser(); + try { + CommandLine cmd = parser.parse(options, args); + + if (cmd.hasOption("p")) { + programmersCount = Integer.parseInt(cmd.getOptionValue("p")); + } + if (cmd.hasOption("w")) { + waitersCount = Integer.parseInt(cmd.getOptionValue("w")); + } + if (cmd.hasOption("f")) { + portionsCount = Integer.parseInt(cmd.getOptionValue("f")); + } + if (cmd.hasOption("t")) { + timeout = Integer.parseInt(cmd.getOptionValue("t")); + } + } catch (ParseException e) { + logger.error("Error parsing arguments", e); + new HelpFormatter().printHelp("dining-philosophers", options); + return; + } + + logger.info( + "Starting simulation with {} programmers, {} waiters, {} portions", + programmersCount, + waitersCount, + portionsCount + ); + + Restaurant restaurant = new Restaurant(programmersCount, waitersCount, portionsCount); + restaurant.start(); + + try { + if (!restaurant.awaitCompletion(timeout, TimeUnit.SECONDS)) { + logger.error("Simulation timed out!"); + restaurant.shutdownNow(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + restaurant.shutdownNow(); + } + + restaurant.printStatistics(); } } \ No newline at end of file diff --git a/src/main/java/org/labs/lunch/DinningTable.java b/src/main/java/org/labs/lunch/DinningTable.java new file mode 100644 index 0000000..29f8191 --- /dev/null +++ b/src/main/java/org/labs/lunch/DinningTable.java @@ -0,0 +1,52 @@ +package org.labs.lunch; + +public class DinningTable { + + private final int programmersCount; + private final Spoon[] spoons; + + public DinningTable(int programmersCount) { + this.programmersCount = programmersCount; + + this.spoons = new Spoon[programmersCount]; + for (int i = 0; i < programmersCount; i++) { + this.spoons[i] = new Spoon(i); + } + + } + + private class SpoonPair { + int firstSpoonId; + int secondSpoonId; + + SpoonPair(int firstSpoonId, int secondSpoonId) { + this.firstSpoonId = firstSpoonId; + this.secondSpoonId = secondSpoonId; + } + } + + private SpoonPair getOrderedSpoonIds(int programmerId) { + int leftSpoonId = programmerId; + int rightSpoonId = (programmerId + 1) % programmersCount; + + if (leftSpoonId < rightSpoonId) { + return new SpoonPair(leftSpoonId, rightSpoonId); + } else { + return new SpoonPair(rightSpoonId, leftSpoonId); + } + } + + void takeSpoons(int programmerId) { + SpoonPair spoonPair = getOrderedSpoonIds(programmerId); + + spoons[spoonPair.firstSpoonId].lock(); + spoons[spoonPair.secondSpoonId].lock(); + } + + void putSpoons(int programmerId) { + SpoonPair spoonPair = getOrderedSpoonIds(programmerId); + + spoons[spoonPair.secondSpoonId].unlock(); + spoons[spoonPair.firstSpoonId].unlock(); + } +} diff --git a/src/main/java/org/labs/lunch/Programmer.java b/src/main/java/org/labs/lunch/Programmer.java new file mode 100644 index 0000000..5049f0a --- /dev/null +++ b/src/main/java/org/labs/lunch/Programmer.java @@ -0,0 +1,84 @@ +package org.labs.lunch; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ThreadLocalRandom; + +public class Programmer implements Runnable { + private static final Logger logger = LoggerFactory.getLogger(Programmer.class); + + private final int id; + private int portionsEaten = 0; + private volatile boolean hasSoupPortion; + private final Restaurant restaurant; + private final ThreadLocalRandom random = ThreadLocalRandom.current(); + private final Object plateMonitor = new Object(); + + private final long minThinkTime = 20; + private final long maxThinkTime = 40; + private final long minEatTime = 10; + private final long maxEatTime = 20; + + public Programmer(int programmerId, Restaurant restaurant) { + this.id = programmerId; + this.hasSoupPortion = false; + this.restaurant = restaurant; + } + + @Override + public void run() { + try { + while (restaurant.isRunning() && (restaurant.getPortionsCount().get() > 0 || hasSoupPortion)) { + think(); + + synchronized (plateMonitor) { + while (!hasSoupPortion && restaurant.isRunning()) { + if (restaurant.getPortionsCount().get() == 0) { + return; + } + + restaurant.requestPortion(this); + plateMonitor.wait(); + } + } + + restaurant.getDinningTable().takeSpoons(id); + eat(); + restaurant.getDinningTable().putSpoons(id); + } + } catch (InterruptedException ex) { + logger.debug("Programmer {} interrupted", id); + Thread.currentThread().interrupt(); + } catch (Exception ex) { + logger.error("Programmer {} encountered unexpected exception", id, ex); + } + } + + private void think() throws InterruptedException { + long duration = random.nextLong(minThinkTime, maxThinkTime); + logger.debug("Programmer {} thinking for {}ms", id, duration); + Thread.sleep(duration); + } + + private void eat() throws InterruptedException { + long duration = random.nextLong(minEatTime, maxEatTime); + logger.debug("Programmer {} eating for {}ms", id, duration); + Thread.sleep(duration); + + hasSoupPortion = false; + portionsEaten += 1; + } + + public void refillPlateWithSoup() { + synchronized (plateMonitor) { + hasSoupPortion = true; + plateMonitor.notifyAll(); + } + } + + + public int getId() { return id; } + public int getPortionsEaten() { return portionsEaten; } + public Object getPlateMonitor() { return plateMonitor; } +} diff --git a/src/main/java/org/labs/lunch/Restaurant.java b/src/main/java/org/labs/lunch/Restaurant.java new file mode 100644 index 0000000..0ff0671 --- /dev/null +++ b/src/main/java/org/labs/lunch/Restaurant.java @@ -0,0 +1,102 @@ +package org.labs.lunch; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class Restaurant { + private static final Logger logger = LoggerFactory.getLogger(Restaurant.class); + + private final AtomicInteger portionsCount; + private final LinkedBlockingQueue portionsAskQueue; + + private final List programmersList; + private final ExecutorService programmersExecutor; + + private final int waitersCount; + private final ExecutorService waitersExecutor; + + private final DinningTable dinningTable; + + private volatile boolean isRunning = true; + + public Restaurant(int programmersCount, int waiterCount, int portionsCount) { + this.portionsCount = new AtomicInteger(portionsCount); + this.portionsAskQueue = new LinkedBlockingQueue<>(); + this.programmersList = new ArrayList<>(); + this.programmersExecutor = Executors.newFixedThreadPool(programmersCount); + this.waitersCount = waiterCount; + this.waitersExecutor = Executors.newFixedThreadPool(waiterCount); + this.dinningTable = new DinningTable(programmersCount); + + for (int i = 0; i < programmersCount; i++) { + Programmer programmer = new Programmer(i, this); + programmersList.add(programmer); + } + } + + public void start() { + for (Programmer programmer : programmersList) { + programmersExecutor.submit(programmer); + } + + for (int i = 0; i < this.waitersCount; i++) { + Waiter waiter = new Waiter(i, this); + waitersExecutor.submit(waiter); + } + } + + public boolean awaitCompletion(long timeout, TimeUnit unit) throws InterruptedException { + programmersExecutor.shutdown(); + boolean completed = programmersExecutor.awaitTermination(timeout, unit); + isRunning = false; + waitersExecutor.shutdownNow(); + return completed; + } + + public void shutdownNow() { + isRunning = false; + programmersExecutor.shutdownNow(); + waitersExecutor.shutdownNow(); + } + + public void printStatistics() { + int totalEaten = 0; + for (Programmer programmer : programmersList) { + int eaten = programmer.getPortionsEaten(); + logger.info("Programmer {} ate {} portions", programmer.getId(), eaten); + totalEaten += eaten; + } + + logger.info("Total portions eaten: {} (remaining: {})", totalEaten, portionsCount.get()); + + double average = totalEaten / (double) programmersList.size(); + double fairnessThreshold = average * 0.01; + + for (Programmer programmer : programmersList) { + int eaten = programmer.getPortionsEaten(); + double deviation = Math.abs(eaten - average); + if (deviation > fairnessThreshold) { + logger.warn("Programmer {} deviation too high: {} > {}", programmer.getId(), deviation, fairnessThreshold); + } + } + } + + void requestPortion(Programmer programmer) throws InterruptedException { + portionsAskQueue.put(programmer); + } + + + public AtomicInteger getPortionsCount() { return portionsCount; } + public LinkedBlockingQueue getPortionsAskQueue() { return portionsAskQueue; } + public List getProgrammersList() { return programmersList; } + public DinningTable getDinningTable() { return dinningTable; } + public boolean isRunning() { return isRunning; } +} diff --git a/src/main/java/org/labs/lunch/Spoon.java b/src/main/java/org/labs/lunch/Spoon.java new file mode 100644 index 0000000..70b8b34 --- /dev/null +++ b/src/main/java/org/labs/lunch/Spoon.java @@ -0,0 +1,21 @@ +package org.labs.lunch; + +import java.util.concurrent.locks.ReentrantLock; + +public class Spoon { + private final int id; + private final ReentrantLock lock; + + public Spoon(int id) { + this.id = id; + lock = new ReentrantLock(true); + } + + public void lock() { + lock.lock(); + } + + public void unlock() { + lock.unlock(); + } +} diff --git a/src/main/java/org/labs/lunch/Waiter.java b/src/main/java/org/labs/lunch/Waiter.java new file mode 100644 index 0000000..079b388 --- /dev/null +++ b/src/main/java/org/labs/lunch/Waiter.java @@ -0,0 +1,56 @@ +package org.labs.lunch; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ThreadLocalRandom; + +public class Waiter implements Runnable { + private static final Logger logger = LoggerFactory.getLogger(Waiter.class); + + private final int id; + private final Restaurant restaurant; + private final ThreadLocalRandom random = ThreadLocalRandom.current(); + + private final long minGetSoupTime = 5; + private final long maxGetSoupTime = 10; + + public Waiter(int waiterId, Restaurant restaurant) { + this.id = waiterId; + this.restaurant = restaurant; + } + + @Override + public void run() { + try { + while (restaurant.isRunning()) { + Programmer programmer = restaurant.getPortionsAskQueue().take(); + + if (!restaurant.isRunning()) break; + + int currentPortions; + do { + currentPortions = restaurant.getPortionsCount().get(); + if (currentPortions <= 0) { + break; + } + } while (!restaurant.getPortionsCount().compareAndSet(currentPortions, currentPortions - 1)); + + if (currentPortions > 0) { + Thread.sleep(random.nextLong(minGetSoupTime, maxGetSoupTime)); + programmer.refillPlateWithSoup(); + logger.debug("Waiter {} served programmer {}", id, programmer.getId()); + } else { + synchronized (programmer.getPlateMonitor()) { + programmer.getPlateMonitor().notifyAll(); + } + } + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + logger.debug("Waiter {} interrupted", id); + } catch (Exception ex) { + logger.error("Waiter {} encountered unexpected error", id, ex); + } + } +} \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..c1f9d32 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/src/test/java/org/labs/lunch/FairnessTest.java b/src/test/java/org/labs/lunch/FairnessTest.java new file mode 100644 index 0000000..8007dcf --- /dev/null +++ b/src/test/java/org/labs/lunch/FairnessTest.java @@ -0,0 +1,44 @@ +package org.labs.lunch; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +class FairnessTest { + + @Test + @Timeout(30) + void testFairDistributionWithSevenProgrammers() throws InterruptedException { + int programmersCount = 7; + int waitersCount = 2; + int portionsCount = 1_000; + + Restaurant restaurant = new Restaurant(programmersCount, waitersCount, portionsCount); + restaurant.start(); + restaurant.awaitCompletion(10, TimeUnit.SECONDS); + + int totalEaten = restaurant.getProgrammersList().stream().mapToInt(Programmer::getPortionsEaten).sum(); + + assertEquals(portionsCount, totalEaten); + + double average = totalEaten / (double) programmersCount; + double expectedMin = average * 0.05; + double expectedMax = average * 1.05; + + for (var programmer : restaurant.getProgrammersList()) { + int eaten = programmer.getPortionsEaten(); + + assertTrue( + eaten >= expectedMin, + String.format("Programmer %d ate too little: %d < %.1f", programmer.getId(), eaten, expectedMin) + ); + assertTrue( + eaten <= expectedMax, + String.format("Programmer %d ate too much: %d > %.1f", programmer.getId(), eaten, expectedMax) + ); + } + } +} \ No newline at end of file diff --git a/src/test/java/org/labs/lunch/RestaurantTest.java b/src/test/java/org/labs/lunch/RestaurantTest.java new file mode 100644 index 0000000..e439811 --- /dev/null +++ b/src/test/java/org/labs/lunch/RestaurantTest.java @@ -0,0 +1,45 @@ +package org.labs.lunch; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +class RestaurantTest { + + @Test + @Timeout(10) + void testSingleProgrammerScenario() throws InterruptedException { + int programmersCount = 1; + int waitersCount = 1; + int portionsCount = 10; + + Restaurant restaurant = new Restaurant(programmersCount, waitersCount, portionsCount); + restaurant.start(); + restaurant.awaitCompletion(5, TimeUnit.SECONDS); + + assertEquals(portionsCount, restaurant.getProgrammersList().getFirst().getPortionsEaten()); + assertEquals(0, restaurant.getPortionsCount().get()); + } + + @Test + @Timeout(10) + void testInterruptionHandling() throws InterruptedException { + int programmersCount = 3; + int waitersCount = 1; + int portionsCount = 1000; + + Restaurant restaurant = new Restaurant(programmersCount, waitersCount, portionsCount); + restaurant.start(); + Thread.sleep(100); + restaurant.shutdownNow(); + + restaurant.awaitCompletion(2, TimeUnit.SECONDS); + int totalEaten = restaurant.getProgrammersList().stream().mapToInt(Programmer::getPortionsEaten).sum(); + + assertTrue(totalEaten < portionsCount); + assertTrue(totalEaten > 0); + } +} \ No newline at end of file From faa1666ca5b933d7deefa55f73d86337e201c30c Mon Sep 17 00:00:00 2001 From: Maria Barkovskaya Date: Wed, 1 Oct 2025 09:42:35 +0300 Subject: [PATCH 3/4] move statistics printing call to finally block --- src/main/java/org/labs/Main.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/labs/Main.java b/src/main/java/org/labs/Main.java index 19c55a9..938d9bf 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -63,8 +63,8 @@ public static void main(String[] args) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); restaurant.shutdownNow(); + } finally { + restaurant.printStatistics(); } - - restaurant.printStatistics(); } } \ No newline at end of file From 78691d5ec8c7703692da772a327870c61cd8357a Mon Sep 17 00:00:00 2001 From: Maria Barkovskaya Date: Wed, 1 Oct 2025 10:05:55 +0300 Subject: [PATCH 4/4] use PriorityBlockingQueue instead of LinkedBlockingQueue --- src/main/java/org/labs/lunch/Restaurant.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/labs/lunch/Restaurant.java b/src/main/java/org/labs/lunch/Restaurant.java index 0ff0671..ac8afad 100644 --- a/src/main/java/org/labs/lunch/Restaurant.java +++ b/src/main/java/org/labs/lunch/Restaurant.java @@ -4,10 +4,11 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -15,7 +16,7 @@ public class Restaurant { private static final Logger logger = LoggerFactory.getLogger(Restaurant.class); private final AtomicInteger portionsCount; - private final LinkedBlockingQueue portionsAskQueue; + private final PriorityBlockingQueue portionsAskQueue; private final List programmersList; private final ExecutorService programmersExecutor; @@ -29,7 +30,10 @@ public class Restaurant { public Restaurant(int programmersCount, int waiterCount, int portionsCount) { this.portionsCount = new AtomicInteger(portionsCount); - this.portionsAskQueue = new LinkedBlockingQueue<>(); + this.portionsAskQueue = new PriorityBlockingQueue<>( + programmersCount, + Comparator.comparingInt(Programmer::getPortionsEaten) + ); this.programmersList = new ArrayList<>(); this.programmersExecutor = Executors.newFixedThreadPool(programmersCount); this.waitersCount = waiterCount; @@ -95,7 +99,7 @@ void requestPortion(Programmer programmer) throws InterruptedException { public AtomicInteger getPortionsCount() { return portionsCount; } - public LinkedBlockingQueue getPortionsAskQueue() { return portionsAskQueue; } + public PriorityBlockingQueue getPortionsAskQueue() { return portionsAskQueue; } public List getProgrammersList() { return programmersList; } public DinningTable getDinningTable() { return dinningTable; } public boolean isRunning() { return isRunning; }