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/README.md b/README.md index d7a6ba3..9db51fe 100644 --- a/README.md +++ b/README.md @@ -1,6 +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 | + # Цели и задачи л/р: + Задача об обедающих философах: Рассмотрим семь программистов, сидящих вокруг круглого стола для обеда. @@ -8,23 +27,25 @@ Однако, чтобы поесть суп, программисту необходимо взять две ложки - справа и слева (он очень голодный). Когда программист поедает суп, ложки остаются занятыми и не могут быть использованы соседними программистами. Программисты чередуют прием еды с обсуждением преподавателей. -Когда суп заканчивается, программист просит одного из двух официантов принести ему еще одну порцию (то есть тарелка супа ограничена). +Когда суп заканчивается, программист просит одного из двух официантов принести ему еще одну порцию (то есть тарелка супа +ограничена). Всего в ресторане есть 1_000_000 порций еды, после чего обед заканчивается. Все программисты должны поесть +- одинаково, чтобы никому не было обидно - - Ваша задача - реализовать симуляцию обеда с использованием языка программирования Java и многопоточности. -Каждый программист должен быть представлен в виде потока, а ложки - в виде общих ресурсов, которые программисты могут захватывать и освобождать. +Каждый программист должен быть представлен в виде потока, а ложки - в виде общих ресурсов, которые программисты могут +захватывать и освобождать. Также не забудьте про официантов и запасы еды. Дополнительное условие -- количество программистов, еды и официантов должно быть параметризируемое. [Это усложнение классической задачи, про которую можно почитать тут](https://en.wikipedia.org/wiki/Dining_philosophers_problem) -Необходимо обеспечить корректное выполнение программы, чтобы избежать состояний взаимной блокировки и гарантировать, что каждый программист получит возможность поесть. +Необходимо обеспечить корректное выполнение программы, чтобы избежать состояний взаимной блокировки и гарантировать, что +каждый программист получит возможность поесть. # Обязательное условие: + * Использование системы сборки Gradle * Код должен быть отлажен и протестирован diff --git a/build.gradle.kts b/build.gradle.kts index bda0d97..8ff4b2b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,8 +10,20 @@ 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("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/Main.java b/src/main/java/org/labs/Main.java index 9917247..bc734a2 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -1,7 +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.DinnerResult; + +@Slf4j public class Main { + + public static final int[] PERCENTILES = {50, 90, 95, 100}; + + @SneakyThrows public static void main(String[] args) { - System.out.println("Hello, World!"); + 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() + ); + + 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")); + } -} \ 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..dd77733 --- /dev/null +++ b/src/main/java/org/labs/Utils.java @@ -0,0 +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(10, 20); + } + + public static int eatTime() { + return random(20, 40); + } + + public static int random(int min, int max) { + return RANDOM.nextInt(min, max); + } + + 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 new file mode 100644 index 0000000..291fa27 --- /dev/null +++ b/src/main/java/org/labs/hierarchy/DinnerFactory.java @@ -0,0 +1,99 @@ +package org.labs.hierarchy; + +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; + +@Slf4j +@RequiredArgsConstructor +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 DinnerResult setupAndRun() { + log.info( + "Starting dinner with \n{} programmers \n{} waiters \n{} servings", + programmersCount, + waitersCount, + servingsCount + ); + + List spoons = createSpoons(); + 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(2, TimeUnit.SECONDS)) { + programmersPool.shutdownNow(); + } + } catch (InterruptedException e) { + programmersPool.shutdownNow(); + } + + try { + if (!waitersPool.awaitTermination(2, TimeUnit.SECONDS)) { + waitersPool.shutdownNow(); + } + } catch (InterruptedException e) { + waitersPool.shutdownNow(); + } + + log.info("All servings were eaten in {} ms", finishTime - startTime); + return new DinnerResult( + programmers.stream() + .map(Programmer::getTotalServings) + .collect(Collectors.toList()), + restaurant.getFoodServings() + ); + } + + @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/DinnerResult.java b/src/main/java/org/labs/hierarchy/DinnerResult.java new file mode 100644 index 0000000..8d83f82 --- /dev/null +++ b/src/main/java/org/labs/hierarchy/DinnerResult.java @@ -0,0 +1,9 @@ +package org.labs.hierarchy; + +import java.util.List; + +public record DinnerResult( + List servingsEaten, + int servingsLeft +) { +} 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..71f0c9c --- /dev/null +++ b/src/main/java/org/labs/hierarchy/FoodRequest.java @@ -0,0 +1,36 @@ +package org.labs.hierarchy; + +import java.util.concurrent.CountDownLatch; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +@RequiredArgsConstructor +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; + + @SneakyThrows + public boolean getServed() { + latch.await(); + return isServed; + } + + public void setServed() { + isServed = true; + latch.countDown(); + } + + 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 new file mode 100644 index 0000000..d4e9070 --- /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, totalServings); +// 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..92e2fa0 --- /dev/null +++ b/src/main/java/org/labs/hierarchy/Restaurant.java @@ -0,0 +1,51 @@ +package org.labs.hierarchy; + +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import lombok.SneakyThrows; + +public class Restaurant { + private final AtomicInteger foodServings; + private final PriorityBlockingQueue requests = new PriorityBlockingQueue<>(); + + public Restaurant( + int servingsCount + ) { + this.foodServings = new AtomicInteger(servingsCount); + } + + @SneakyThrows + public FoodRequest requestFood(int clientId, int alreadyEaten) { + FoodRequest request = new FoodRequest(clientId, alreadyEaten); + 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..79034eb --- /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"); + } +} 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()) + ); + } +}