From 5b9327533f286a4e4d7b02ba7e17e368f930201f Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:27:29 +0000 Subject: [PATCH 1/5] 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 011f2c2506eacc96fd1a3985d04ff5ba6f2ce455 Mon Sep 17 00:00:00 2001 From: railolog Date: Sat, 4 Oct 2025 23:18:06 +0300 Subject: [PATCH 2/5] hierarchy implementation --- .gitignore | 1 + build.gradle.kts | 9 ++ src/main/java/org/labs/Main.java | 25 ++++- src/main/java/org/labs/Utils.java | 31 +++++++ .../org/labs/hierarchy/DinnerFactory.java | 93 +++++++++++++++++++ .../org/labs/hierarchy/DinnerSummary.java | 8 ++ .../java/org/labs/hierarchy/FoodRequest.java | 29 ++++++ .../java/org/labs/hierarchy/Programmer.java | 93 +++++++++++++++++++ .../java/org/labs/hierarchy/Restaurant.java | 45 +++++++++ src/main/java/org/labs/hierarchy/Spoon.java | 14 +++ src/main/java/org/labs/hierarchy/State.java | 7 ++ src/main/java/org/labs/hierarchy/Waiter.java | 36 +++++++ 12 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/labs/Utils.java create mode 100644 src/main/java/org/labs/hierarchy/DinnerFactory.java create mode 100644 src/main/java/org/labs/hierarchy/DinnerSummary.java create mode 100644 src/main/java/org/labs/hierarchy/FoodRequest.java create mode 100644 src/main/java/org/labs/hierarchy/Programmer.java create mode 100644 src/main/java/org/labs/hierarchy/Restaurant.java create mode 100644 src/main/java/org/labs/hierarchy/Spoon.java create mode 100644 src/main/java/org/labs/hierarchy/State.java create mode 100644 src/main/java/org/labs/hierarchy/Waiter.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..9dcb479 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,15 @@ repositories { } dependencies { + implementation("org.slf4j:slf4j-api:2.0.16") + implementation("ch.qos.logback:logback-classic:1.5.13") + + compileOnly("org.projectlombok:lombok:1.18.42") + annotationProcessor("org.projectlombok:lombok:1.18.42") + + testCompileOnly("org.projectlombok:lombok:1.18.42") + testAnnotationProcessor("org.projectlombok:lombok:1.18.42") + testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") } diff --git a/src/main/java/org/labs/Main.java b/src/main/java/org/labs/Main.java index 9917247..158b150 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -1,7 +1,28 @@ package org.labs; +import java.util.concurrent.Executors; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.labs.hierarchy.DinnerFactory; +import org.labs.hierarchy.DinnerSummary; + +@Slf4j public class Main { + + @SneakyThrows public static void main(String[] args) { - System.out.println("Hello, World!"); + int programmersCount = 1_000; + int waitersCount = 10; + + DinnerFactory dinnerFactory = new DinnerFactory( + programmersCount, + waitersCount, + Executors.newVirtualThreadPerTaskExecutor(), + Executors.newVirtualThreadPerTaskExecutor() + ); + + DinnerSummary dinnerSummary = dinnerFactory.setupAndRun(); + log.info("Dinner summary: {}", dinnerSummary); } -} \ No newline at end of file +} diff --git a/src/main/java/org/labs/Utils.java b/src/main/java/org/labs/Utils.java new file mode 100644 index 0000000..959ccf1 --- /dev/null +++ b/src/main/java/org/labs/Utils.java @@ -0,0 +1,31 @@ +package org.labs; + +import java.util.List; +import java.util.Random; + +public class Utils { + private static final Random RANDOM = new Random(); + + public static int discussTime() { + return random(1, 2); + } + + public static int eatTime() { + return random(2, 4); + } + + public static int random(int min, int max) { + return RANDOM.nextInt(min, max); + } + + public static int findMedian(List numbers) { + int size = numbers.size(); + if (size % 2 == 1) { + return numbers.get(size / 2); + } else { + int mid1 = numbers.get(size / 2 - 1); + int mid2 = numbers.get(size / 2); + return (mid1 + mid2) / 2; + } + } +} diff --git a/src/main/java/org/labs/hierarchy/DinnerFactory.java b/src/main/java/org/labs/hierarchy/DinnerFactory.java new file mode 100644 index 0000000..efad4b4 --- /dev/null +++ b/src/main/java/org/labs/hierarchy/DinnerFactory.java @@ -0,0 +1,93 @@ +package org.labs.hierarchy; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.labs.Utils; + +@Slf4j +@RequiredArgsConstructor +public class DinnerFactory { + + private final int programmersCount; + private final int waitersCount; + private final ExecutorService programmersPool; + private final ExecutorService waitersPool; + + @SneakyThrows + public DinnerSummary setupAndRun() { + List spoons = createSpoons(); + Restaurant restaurant = new Restaurant(); + List programmers = createProgrammers(restaurant, spoons); + List waiters = createWaiters(restaurant); + + programmers.forEach(programmersPool::submit); + waiters.forEach(waitersPool::submit); + + monitorRestaurant(restaurant); + + programmersPool.shutdown(); + waitersPool.shutdown(); + try { + if (!programmersPool.awaitTermination(10, TimeUnit.SECONDS)) { + programmersPool.shutdownNow(); + } + } catch (InterruptedException e) { + programmersPool.shutdownNow(); + } +// programmers.forEach(p -> log.info("Programmer {} eat total {} servings", p.getId(), p.getTotalServings())); + try { + if (!waitersPool.awaitTermination(2, TimeUnit.SECONDS)) { + waitersPool.shutdownNow(); + } + } catch (InterruptedException e) { + waitersPool.shutdownNow(); + } + + Integer max = programmers.stream() + .map(Programmer::getTotalServings) + .max(Integer::compareTo) + .get(); + Integer min = programmers.stream() + .map(Programmer::getTotalServings) + .min(Integer::compareTo) + .get(); + + return new DinnerSummary( + min, + max, + Utils.findMedian(programmers.stream().map(Programmer::getTotalServings).sorted().toList()) + ); + } + + @SneakyThrows + private void monitorRestaurant(Restaurant restaurant) { + while (restaurant.isFoodAvailable()) { + Thread.sleep(500); + log.info("Restaurant have {} servings left", restaurant.getFoodServings()); + } + } + + private List createSpoons() { + return IntStream.range(0, programmersCount) + .mapToObj(Spoon::new) + .toList(); + } + + private List createProgrammers(Restaurant restaurant, List spoons) { + return IntStream.range(0, programmersCount) + .mapToObj(i -> new Programmer(i, spoons.get(i), spoons.get((i + 1) % programmersCount), restaurant)) + .toList(); + } + + private List createWaiters(Restaurant restaurant) { + return IntStream.range(0, waitersCount) + .mapToObj(i -> new Waiter(restaurant)) + .toList(); + } +} diff --git a/src/main/java/org/labs/hierarchy/DinnerSummary.java b/src/main/java/org/labs/hierarchy/DinnerSummary.java new file mode 100644 index 0000000..29fb24e --- /dev/null +++ b/src/main/java/org/labs/hierarchy/DinnerSummary.java @@ -0,0 +1,8 @@ +package org.labs.hierarchy; + +public record DinnerSummary( + int minEatServings, + int maxEatServings, + int medianEatServings +) { +} diff --git a/src/main/java/org/labs/hierarchy/FoodRequest.java b/src/main/java/org/labs/hierarchy/FoodRequest.java new file mode 100644 index 0000000..592c780 --- /dev/null +++ b/src/main/java/org/labs/hierarchy/FoodRequest.java @@ -0,0 +1,29 @@ +package org.labs.hierarchy; + +import java.util.concurrent.CountDownLatch; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +@RequiredArgsConstructor +public final class FoodRequest { + private final int clientId; + private final CountDownLatch latch = new CountDownLatch(1); + private boolean isServed = false; + + @SneakyThrows + public boolean getServed() { + latch.await(); + return isServed; + } + + public void setServed() { + isServed = true; + latch.countDown(); + } + + public void setUnserved() { + isServed = false; + latch.countDown(); + } +} diff --git a/src/main/java/org/labs/hierarchy/Programmer.java b/src/main/java/org/labs/hierarchy/Programmer.java new file mode 100644 index 0000000..1b0ef4e --- /dev/null +++ b/src/main/java/org/labs/hierarchy/Programmer.java @@ -0,0 +1,93 @@ +package org.labs.hierarchy; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.labs.Utils; + +@Slf4j +@RequiredArgsConstructor +public class Programmer implements Runnable { + + @Getter + private final int id; + private final Spoon leftSpoon; + private final Spoon rightSpoon; + private final Restaurant restaurant; + + @Getter + private int totalServings = 0; + private State state = State.DISCUSSING; + private volatile boolean isFinished = false; + + + @Override + public void run() { + while (!isFinished) { + switch (state) { + case DISCUSSING -> discuss(); + case HUNGRY -> requestFood(); + case EATING -> eat(); + } + } +// log.info("Programmer {} leaving execution method", id); + } + + @SneakyThrows + private void eat() { + takeSpoons(); + int eatTimeMs = Utils.eatTime(); +// log.info("Programmer {} is eating for {} ms", id, eatTimeMs); + Thread.sleep(eatTimeMs); +// log.info("Programmer {} finished its meal", id); + releaseSpoons(); + + state = State.DISCUSSING; + totalServings++; + } + + private void takeSpoons() { + if (leftSpoon.getNumber() < rightSpoon.getNumber()) { + leftSpoon.getLock().lock(); + rightSpoon.getLock().lock(); + } else { + rightSpoon.getLock().lock(); + leftSpoon.getLock().lock(); + } + } + + private void releaseSpoons() { + leftSpoon.getLock().unlock(); + rightSpoon.getLock().unlock(); + } + + @SneakyThrows + private void discuss() { + int discussTimeMs = Utils.discussTime(); +// log.info("Programmer {} is discussing for {} ms", id, discussTimeMs); + Thread.sleep(discussTimeMs); +// log.info("Programmer {} finished discussing", id); + + state = State.HUNGRY; + } + + @SneakyThrows + private void requestFood() { + FoodRequest request = restaurant.requestFood(id); +// log.info("Programmer {} is waiting for food", id); + boolean served = request.getServed(); + + if (!served) { + isFinished = true; +// log.info("The dinner is over, programmer is shutting down"); + } else { +// log.info("Programmer {} received food", id); + receiveFood(); + } + } + + private void receiveFood() { + state = State.EATING; + } +} \ No newline at end of file diff --git a/src/main/java/org/labs/hierarchy/Restaurant.java b/src/main/java/org/labs/hierarchy/Restaurant.java new file mode 100644 index 0000000..4f8c592 --- /dev/null +++ b/src/main/java/org/labs/hierarchy/Restaurant.java @@ -0,0 +1,45 @@ +package org.labs.hierarchy; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import lombok.SneakyThrows; + +public class Restaurant { + private final AtomicInteger foodServings = new AtomicInteger(1_000_000); + private final LinkedBlockingQueue requests = new LinkedBlockingQueue<>(); + + @SneakyThrows + public FoodRequest requestFood(int clientId) { + FoodRequest request = new FoodRequest(clientId); + requests.put(request); + return request; + } + + @SneakyThrows + public FoodRequest getNextRequest() { + return requests.poll(1, TimeUnit.SECONDS); + } + + public boolean isFoodAvailable() { + return foodServings.get() > 0; + } + + public boolean getFood() { + int servings = foodServings.get(); + while (servings > 0) { + boolean success = foodServings.compareAndSet(servings, servings - 1); + if (success) { + return true; + } else { + servings = foodServings.get(); + } + } + return false; + } + + public int getFoodServings() { + return foodServings.get(); + } +} diff --git a/src/main/java/org/labs/hierarchy/Spoon.java b/src/main/java/org/labs/hierarchy/Spoon.java new file mode 100644 index 0000000..a5e8c67 --- /dev/null +++ b/src/main/java/org/labs/hierarchy/Spoon.java @@ -0,0 +1,14 @@ +package org.labs.hierarchy; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public final class Spoon { + private final int number; + private final Lock lock = new ReentrantLock(); +} diff --git a/src/main/java/org/labs/hierarchy/State.java b/src/main/java/org/labs/hierarchy/State.java new file mode 100644 index 0000000..da34379 --- /dev/null +++ b/src/main/java/org/labs/hierarchy/State.java @@ -0,0 +1,7 @@ +package org.labs.hierarchy; + +public enum State { + DISCUSSING, + HUNGRY, + EATING, +} diff --git a/src/main/java/org/labs/hierarchy/Waiter.java b/src/main/java/org/labs/hierarchy/Waiter.java new file mode 100644 index 0000000..b0f586d --- /dev/null +++ b/src/main/java/org/labs/hierarchy/Waiter.java @@ -0,0 +1,36 @@ +package org.labs.hierarchy; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class Waiter implements Runnable { + + private final Restaurant restaurant; + + private volatile boolean isFinished = false; + + @Override + public void run() { + while (!isFinished) { + FoodRequest request = restaurant.getNextRequest(); +// log.info("Waiter is serving request: {}", request); + if (request != null) { + boolean food = restaurant.getFood(); + if (food) { + request.setServed(); + } else { + request.setUnserved(); + } + } else { + log.info("Waiter got no new request, checking if food is available"); + if (!restaurant.isFoodAvailable()) { + log.info("Waiter is finishing"); + isFinished = true; + } + } + } + log.info("Waiter leaving execution method"); + } +} From e9bc221ccca302daceefb0dd30e251c013a934c4 Mon Sep 17 00:00:00 2001 From: railolog Date: Tue, 7 Oct 2025 00:48:06 +0300 Subject: [PATCH 3/5] add more configuration, run with different setups, summary --- README.md | 30 ++++++-- src/main/java/org/labs/Main.java | 68 +++++++++++++++++-- src/main/java/org/labs/Utils.java | 52 +++++++++++--- .../org/labs/hierarchy/DinnerFactory.java | 41 ++++++----- .../java/org/labs/hierarchy/DinnerResult.java | 8 +++ .../org/labs/hierarchy/DinnerSummary.java | 8 --- .../java/org/labs/hierarchy/Restaurant.java | 8 ++- src/main/java/org/labs/hierarchy/Waiter.java | 6 +- 8 files changed, 171 insertions(+), 50 deletions(-) create mode 100644 src/main/java/org/labs/hierarchy/DinnerResult.java delete mode 100644 src/main/java/org/labs/hierarchy/DinnerSummary.java diff --git a/README.md b/README.md index e974d43..9db51fe 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,25 @@ [![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/qcWcnElX) + # Java concurrency +# Результаты + +В последних столбца приведены перцентили распределения кол-ва съеденных программистами порций + +| Программистов | Официантов | Порций | Время (ms) | p50 | p90 | p95 | p100 | +|---------------|------------|---------|------------|--------|--------|--------|--------| +| 2 | 1 | 300 | 10084 | 150.00 | 150.00 | 150.00 | 150.00 | +| 20 | 1 | 300 | 1514 | 15.00 | 16.30 | 19.00 | 19.00 | +| 200 | 1 | 300 | 505 | 1.50 | 2.00 | 2.00 | 2.00 | +| 10000 | 1 | 600000 | 8030 | 60.00 | 63.00 | 64.00 | 124.00 | +| 10000 | 10 | 600000 | 8531 | 60.00 | 64.00 | 65.00 | 128.00 | +| 10000 | 1000 | 600000 | 8536 | 60.00 | 64.00 | 65.00 | 128.00 | +| 10000 | 1000 | 1000000 | 16532 | 99.00 | 105.00 | 106.00 | 251.00 | +| 20000 | 1000 | 1000000 | 6551 | 50.00 | 53.00 | 54.00 | 68.00 | +| 200000 | 1000 | 1000000 | 8123 | 5.00 | 6.00 | 6.00 | 7.00 | + # Цели и задачи л/р: + Задача об обедающих философах: Рассмотрим семь программистов, сидящих вокруг круглого стола для обеда. @@ -9,23 +27,25 @@ Однако, чтобы поесть суп, программисту необходимо взять две ложки - справа и слева (он очень голодный). Когда программист поедает суп, ложки остаются занятыми и не могут быть использованы соседними программистами. Программисты чередуют прием еды с обсуждением преподавателей. -Когда суп заканчивается, программист просит одного из двух официантов принести ему еще одну порцию (то есть тарелка супа ограничена). +Когда суп заканчивается, программист просит одного из двух официантов принести ему еще одну порцию (то есть тарелка супа +ограничена). Всего в ресторане есть 1_000_000 порций еды, после чего обед заканчивается. Все программисты должны поесть +- одинаково, чтобы никому не было обидно - - Ваша задача - реализовать симуляцию обеда с использованием языка программирования Java и многопоточности. -Каждый программист должен быть представлен в виде потока, а ложки - в виде общих ресурсов, которые программисты могут захватывать и освобождать. +Каждый программист должен быть представлен в виде потока, а ложки - в виде общих ресурсов, которые программисты могут +захватывать и освобождать. Также не забудьте про официантов и запасы еды. Дополнительное условие -- количество программистов, еды и официантов должно быть параметризируемое. [Это усложнение классической задачи, про которую можно почитать тут](https://en.wikipedia.org/wiki/Dining_philosophers_problem) -Необходимо обеспечить корректное выполнение программы, чтобы избежать состояний взаимной блокировки и гарантировать, что каждый программист получит возможность поесть. +Необходимо обеспечить корректное выполнение программы, чтобы избежать состояний взаимной блокировки и гарантировать, что +каждый программист получит возможность поесть. # Обязательное условие: + * Использование системы сборки Gradle * Код должен быть отлажен и протестирован diff --git a/src/main/java/org/labs/Main.java b/src/main/java/org/labs/Main.java index 158b150..bc734a2 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -1,28 +1,86 @@ package org.labs; +import java.util.Map; import java.util.concurrent.Executors; +import java.util.stream.Collectors; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.labs.hierarchy.DinnerFactory; -import org.labs.hierarchy.DinnerSummary; +import org.labs.hierarchy.DinnerResult; @Slf4j public class Main { + public static final int[] PERCENTILES = {50, 90, 95, 100}; + @SneakyThrows public static void main(String[] args) { - int programmersCount = 1_000; - int waitersCount = 10; + int programmersCount; + int waitersCount; + int servings; + programmersCount = 2; + waitersCount = 1; + servings = 300; + + dinnerSample(programmersCount, waitersCount, servings); + + programmersCount = 20; + dinnerSample(programmersCount, waitersCount, servings); + + programmersCount = 200; + dinnerSample(programmersCount, waitersCount, servings); +// + programmersCount = 10_000; + waitersCount = 1; + servings = 600_000; + + dinnerSample(programmersCount, waitersCount, servings); + + waitersCount = 10; + dinnerSample(programmersCount, waitersCount, servings); + + waitersCount = 1000; + dinnerSample(programmersCount, waitersCount, servings); + + programmersCount = 10_000; + waitersCount = 1000; + servings = 1_000_000; + + dinnerSample(programmersCount, waitersCount, servings); + programmersCount = 20_000; + dinnerSample(programmersCount, waitersCount, servings); + + programmersCount = 200_000; + dinnerSample(programmersCount, waitersCount, servings); + } + + private static void dinnerSample(int programmersCount, int waitersCount, int servings) { DinnerFactory dinnerFactory = new DinnerFactory( programmersCount, waitersCount, + servings, Executors.newVirtualThreadPerTaskExecutor(), Executors.newVirtualThreadPerTaskExecutor() ); - DinnerSummary dinnerSummary = dinnerFactory.setupAndRun(); - log.info("Dinner summary: {}", dinnerSummary); + DinnerResult dinnerResult = dinnerFactory.setupAndRun(); + logResults(dinnerResult); + } + + + private static void logResults(DinnerResult dinnerResult) { + log.info("Each programmer had servings by percentiles: \n{}", printPercentiles(Utils.calculatePercentiles( + dinnerResult.servingsEaten(), + PERCENTILES + ))); + } + + private static String printPercentiles(Map percentilesTable) { + return percentilesTable.entrySet().stream() + .map(e -> String.format(" p%d: %10.2f", e.getKey(), e.getValue())) + .collect(Collectors.joining("\n")); + } } diff --git a/src/main/java/org/labs/Utils.java b/src/main/java/org/labs/Utils.java index 959ccf1..dd77733 100644 --- a/src/main/java/org/labs/Utils.java +++ b/src/main/java/org/labs/Utils.java @@ -1,31 +1,63 @@ package org.labs; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Random; public class Utils { private static final Random RANDOM = new Random(); public static int discussTime() { - return random(1, 2); + return random(10, 20); } public static int eatTime() { - return random(2, 4); + return random(20, 40); } public static int random(int min, int max) { return RANDOM.nextInt(min, max); } - public static int findMedian(List numbers) { - int size = numbers.size(); - if (size % 2 == 1) { - return numbers.get(size / 2); - } else { - int mid1 = numbers.get(size / 2 - 1); - int mid2 = numbers.get(size / 2); - return (mid1 + mid2) / 2; + public static Map calculatePercentiles(List data, int... percentiles) { + List sortedData = new ArrayList<>(data); + Collections.sort(sortedData); + + Map results = new LinkedHashMap<>(); + for (int p : percentiles) { + results.put(p, calculatePercentile(sortedData, p)); + } + return results; + } + + private static double calculatePercentile(List sortedList, int percentile) { + if (sortedList == null || sortedList.isEmpty()) { + throw new IllegalArgumentException("List cannot be null or empty"); + } + if (percentile < 0 || percentile > 100) { + throw new IllegalArgumentException("Percentile must be between 0 and 100"); + } + + int n = sortedList.size(); + + if (percentile == 0) return sortedList.getFirst(); + if (percentile == 100) return sortedList.get(n - 1); + + double rank = percentile / 100.0 * (n - 1); + int lowerIndex = (int) Math.floor(rank); + int upperIndex = (int) Math.ceil(rank); + + if (lowerIndex == upperIndex) { + return sortedList.get(lowerIndex); } + + double lowerValue = sortedList.get(lowerIndex); + double upperValue = sortedList.get(upperIndex); + double fraction = rank - lowerIndex; + + return lowerValue + fraction * (upperValue - lowerValue); } } diff --git a/src/main/java/org/labs/hierarchy/DinnerFactory.java b/src/main/java/org/labs/hierarchy/DinnerFactory.java index efad4b4..0bc58cd 100644 --- a/src/main/java/org/labs/hierarchy/DinnerFactory.java +++ b/src/main/java/org/labs/hierarchy/DinnerFactory.java @@ -3,12 +3,12 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.labs.Utils; @Slf4j @RequiredArgsConstructor @@ -16,31 +16,44 @@ public class DinnerFactory { private final int programmersCount; private final int waitersCount; + private final int servingsCount; private final ExecutorService programmersPool; private final ExecutorService waitersPool; @SneakyThrows - public DinnerSummary setupAndRun() { + public DinnerResult setupAndRun() { + log.info( + "Starting dinner with \n{} programmers \n{} waiters \n{} servings", + programmersCount, + waitersCount, + servingsCount + ); + List spoons = createSpoons(); - Restaurant restaurant = new Restaurant(); + Restaurant restaurant = new Restaurant(servingsCount); List programmers = createProgrammers(restaurant, spoons); List waiters = createWaiters(restaurant); + long startTime = (long) (System.nanoTime() / 1e6); + programmers.forEach(programmersPool::submit); waiters.forEach(waitersPool::submit); + // blocking current thread monitorRestaurant(restaurant); + long finishTime = (long) (System.nanoTime() / 1e6); + programmersPool.shutdown(); waitersPool.shutdown(); try { - if (!programmersPool.awaitTermination(10, TimeUnit.SECONDS)) { + if (!programmersPool.awaitTermination(2, TimeUnit.SECONDS)) { programmersPool.shutdownNow(); } } catch (InterruptedException e) { programmersPool.shutdownNow(); } -// programmers.forEach(p -> log.info("Programmer {} eat total {} servings", p.getId(), p.getTotalServings())); + try { if (!waitersPool.awaitTermination(2, TimeUnit.SECONDS)) { waitersPool.shutdownNow(); @@ -49,19 +62,11 @@ public DinnerSummary setupAndRun() { waitersPool.shutdownNow(); } - Integer max = programmers.stream() - .map(Programmer::getTotalServings) - .max(Integer::compareTo) - .get(); - Integer min = programmers.stream() - .map(Programmer::getTotalServings) - .min(Integer::compareTo) - .get(); - - return new DinnerSummary( - min, - max, - Utils.findMedian(programmers.stream().map(Programmer::getTotalServings).sorted().toList()) + log.info("All servings were eaten in {} ms", finishTime - startTime); + return new DinnerResult( + programmers.stream() + .map(Programmer::getTotalServings) + .collect(Collectors.toList()) ); } diff --git a/src/main/java/org/labs/hierarchy/DinnerResult.java b/src/main/java/org/labs/hierarchy/DinnerResult.java new file mode 100644 index 0000000..28a68d9 --- /dev/null +++ b/src/main/java/org/labs/hierarchy/DinnerResult.java @@ -0,0 +1,8 @@ +package org.labs.hierarchy; + +import java.util.List; + +public record DinnerResult( + List servingsEaten +) { +} diff --git a/src/main/java/org/labs/hierarchy/DinnerSummary.java b/src/main/java/org/labs/hierarchy/DinnerSummary.java deleted file mode 100644 index 29fb24e..0000000 --- a/src/main/java/org/labs/hierarchy/DinnerSummary.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.labs.hierarchy; - -public record DinnerSummary( - int minEatServings, - int maxEatServings, - int medianEatServings -) { -} diff --git a/src/main/java/org/labs/hierarchy/Restaurant.java b/src/main/java/org/labs/hierarchy/Restaurant.java index 4f8c592..c58f748 100644 --- a/src/main/java/org/labs/hierarchy/Restaurant.java +++ b/src/main/java/org/labs/hierarchy/Restaurant.java @@ -7,9 +7,15 @@ import lombok.SneakyThrows; public class Restaurant { - private final AtomicInteger foodServings = new AtomicInteger(1_000_000); + private final AtomicInteger foodServings; private final LinkedBlockingQueue requests = new LinkedBlockingQueue<>(); + public Restaurant( + int servingsCount + ) { + this.foodServings = new AtomicInteger(servingsCount); + } + @SneakyThrows public FoodRequest requestFood(int clientId) { FoodRequest request = new FoodRequest(clientId); diff --git a/src/main/java/org/labs/hierarchy/Waiter.java b/src/main/java/org/labs/hierarchy/Waiter.java index b0f586d..79034eb 100644 --- a/src/main/java/org/labs/hierarchy/Waiter.java +++ b/src/main/java/org/labs/hierarchy/Waiter.java @@ -24,13 +24,13 @@ public void run() { request.setUnserved(); } } else { - log.info("Waiter got no new request, checking if food is available"); +// log.info("Waiter got no new request, checking if food is available"); if (!restaurant.isFoodAvailable()) { - log.info("Waiter is finishing"); +// log.info("Waiter is finishing"); isFinished = true; } } } - log.info("Waiter leaving execution method"); +// log.info("Waiter leaving execution method"); } } From 9fac7f42ec86f3ee695d94d49c44da83bf58f5c6 Mon Sep 17 00:00:00 2001 From: railolog Date: Tue, 7 Oct 2025 01:14:43 +0300 Subject: [PATCH 4/5] add tests --- build.gradle.kts | 3 + .../org/labs/hierarchy/DinnerFactory.java | 3 +- .../java/org/labs/hierarchy/DinnerResult.java | 3 +- src/test/java/org/labs/DinnerTest.java | 68 +++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/labs/DinnerTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 9dcb479..8ff4b2b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,8 +19,11 @@ dependencies { testCompileOnly("org.projectlombok:lombok:1.18.42") testAnnotationProcessor("org.projectlombok:lombok:1.18.42") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") + + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") } tasks.test { diff --git a/src/main/java/org/labs/hierarchy/DinnerFactory.java b/src/main/java/org/labs/hierarchy/DinnerFactory.java index 0bc58cd..fd3f8ef 100644 --- a/src/main/java/org/labs/hierarchy/DinnerFactory.java +++ b/src/main/java/org/labs/hierarchy/DinnerFactory.java @@ -66,7 +66,8 @@ public DinnerResult setupAndRun() { return new DinnerResult( programmers.stream() .map(Programmer::getTotalServings) - .collect(Collectors.toList()) + .collect(Collectors.toList()), + restaurant.getFoodServings() ); } diff --git a/src/main/java/org/labs/hierarchy/DinnerResult.java b/src/main/java/org/labs/hierarchy/DinnerResult.java index 28a68d9..8d83f82 100644 --- a/src/main/java/org/labs/hierarchy/DinnerResult.java +++ b/src/main/java/org/labs/hierarchy/DinnerResult.java @@ -3,6 +3,7 @@ import java.util.List; public record DinnerResult( - List servingsEaten + List servingsEaten, + int servingsLeft ) { } diff --git a/src/test/java/org/labs/DinnerTest.java b/src/test/java/org/labs/DinnerTest.java new file mode 100644 index 0000000..17a0a7c --- /dev/null +++ b/src/test/java/org/labs/DinnerTest.java @@ -0,0 +1,68 @@ +package org.labs; + +import java.util.Collections; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.Test; +import org.labs.hierarchy.DinnerFactory; +import org.labs.hierarchy.DinnerResult; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DinnerTest { + + @Test + void dinnerForOne_allServingsEaten() { + int programmersCount = 1; + int waitersCount = 1; + int servings = 20; + DinnerFactory dinnerFactory = new DinnerFactory( + programmersCount, + waitersCount, + servings, + Executors.newVirtualThreadPerTaskExecutor(), + Executors.newVirtualThreadPerTaskExecutor() + ); + DinnerResult dinnerResult = dinnerFactory.setupAndRun(); + + assertEquals(0, dinnerResult.servingsLeft()); + } + + @Test + void dinner_manyWaiters_allServingsEaten() { + int programmersCount = 2; + int waitersCount = 100; + int servings = 20; + DinnerFactory dinnerFactory = new DinnerFactory( + programmersCount, + waitersCount, + servings, + Executors.newVirtualThreadPerTaskExecutor(), + Executors.newVirtualThreadPerTaskExecutor() + ); + DinnerResult dinnerResult = dinnerFactory.setupAndRun(); + + assertEquals(0, dinnerResult.servingsLeft()); + } + + @Test + void dinner_fairlyDistributed() { + int programmersCount = 20; + int waitersCount = 5; + int servings = 1000; + DinnerFactory dinnerFactory = new DinnerFactory( + programmersCount, + waitersCount, + servings, + Executors.newVirtualThreadPerTaskExecutor(), + Executors.newVirtualThreadPerTaskExecutor() + ); + DinnerResult dinnerResult = dinnerFactory.setupAndRun(); + + assertEquals(0, dinnerResult.servingsLeft()); + assertTrue( + Collections.min(dinnerResult.servingsEaten()) * 2 >= Collections.max(dinnerResult.servingsEaten()) + ); + } +} From 0c5a61a9570f94ebd997e64b66bf3db983e4ca4f Mon Sep 17 00:00:00 2001 From: railolog Date: Wed, 8 Oct 2025 16:50:32 +0300 Subject: [PATCH 5/5] switch to PriorityQueue --- src/main/java/org/labs/hierarchy/DinnerFactory.java | 2 +- src/main/java/org/labs/hierarchy/FoodRequest.java | 9 ++++++++- src/main/java/org/labs/hierarchy/Programmer.java | 2 +- src/main/java/org/labs/hierarchy/Restaurant.java | 8 ++++---- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/labs/hierarchy/DinnerFactory.java b/src/main/java/org/labs/hierarchy/DinnerFactory.java index fd3f8ef..291fa27 100644 --- a/src/main/java/org/labs/hierarchy/DinnerFactory.java +++ b/src/main/java/org/labs/hierarchy/DinnerFactory.java @@ -75,7 +75,7 @@ public DinnerResult setupAndRun() { private void monitorRestaurant(Restaurant restaurant) { while (restaurant.isFoodAvailable()) { Thread.sleep(500); - log.info("Restaurant have {} servings left", restaurant.getFoodServings()); +// log.info("Restaurant have {} servings left", restaurant.getFoodServings()); } } diff --git a/src/main/java/org/labs/hierarchy/FoodRequest.java b/src/main/java/org/labs/hierarchy/FoodRequest.java index 592c780..71f0c9c 100644 --- a/src/main/java/org/labs/hierarchy/FoodRequest.java +++ b/src/main/java/org/labs/hierarchy/FoodRequest.java @@ -6,8 +6,9 @@ import lombok.SneakyThrows; @RequiredArgsConstructor -public final class FoodRequest { +public final class FoodRequest implements Comparable { private final int clientId; + private final int alreadyEaten; private final CountDownLatch latch = new CountDownLatch(1); private boolean isServed = false; @@ -26,4 +27,10 @@ public void setUnserved() { isServed = false; latch.countDown(); } + + + @Override + public int compareTo(FoodRequest o) { + return Integer.compare(alreadyEaten, o.alreadyEaten); + } } diff --git a/src/main/java/org/labs/hierarchy/Programmer.java b/src/main/java/org/labs/hierarchy/Programmer.java index 1b0ef4e..d4e9070 100644 --- a/src/main/java/org/labs/hierarchy/Programmer.java +++ b/src/main/java/org/labs/hierarchy/Programmer.java @@ -74,7 +74,7 @@ private void discuss() { @SneakyThrows private void requestFood() { - FoodRequest request = restaurant.requestFood(id); + FoodRequest request = restaurant.requestFood(id, totalServings); // log.info("Programmer {} is waiting for food", id); boolean served = request.getServed(); diff --git a/src/main/java/org/labs/hierarchy/Restaurant.java b/src/main/java/org/labs/hierarchy/Restaurant.java index c58f748..92e2fa0 100644 --- a/src/main/java/org/labs/hierarchy/Restaurant.java +++ b/src/main/java/org/labs/hierarchy/Restaurant.java @@ -1,6 +1,6 @@ package org.labs.hierarchy; -import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -8,7 +8,7 @@ public class Restaurant { private final AtomicInteger foodServings; - private final LinkedBlockingQueue requests = new LinkedBlockingQueue<>(); + private final PriorityBlockingQueue requests = new PriorityBlockingQueue<>(); public Restaurant( int servingsCount @@ -17,8 +17,8 @@ public Restaurant( } @SneakyThrows - public FoodRequest requestFood(int clientId) { - FoodRequest request = new FoodRequest(clientId); + public FoodRequest requestFood(int clientId, int alreadyEaten) { + FoodRequest request = new FoodRequest(clientId, alreadyEaten); requests.put(request); return request; }