diff --git a/README.md b/README.md index d7a6ba3..1e8049d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/qcWcnElX) + # Java concurrency # Цели и задачи л/р: + Задача об обедающих философах: Рассмотрим семь программистов, сидящих вокруг круглого стола для обеда. @@ -8,24 +11,115 @@ Однако, чтобы поесть суп, программисту необходимо взять две ложки - справа и слева (он очень голодный). Когда программист поедает суп, ложки остаются занятыми и не могут быть использованы соседними программистами. Программисты чередуют прием еды с обсуждением преподавателей. -Когда суп заканчивается, программист просит одного из двух официантов принести ему еще одну порцию (то есть тарелка супа ограничена). +Когда суп заканчивается, программист просит одного из двух официантов принести ему еще одну порцию (то есть тарелка супа +ограничена). Всего в ресторане есть 1_000_000 порций еды, после чего обед заканчивается. Все программисты должны поесть +- одинаково, чтобы никому не было обидно - - Ваша задача - реализовать симуляцию обеда с использованием языка программирования Java и многопоточности. -Каждый программист должен быть представлен в виде потока, а ложки - в виде общих ресурсов, которые программисты могут захватывать и освобождать. +Каждый программист должен быть представлен в виде потока, а ложки - в виде общих ресурсов, которые программисты могут +захватывать и освобождать. Также не забудьте про официантов и запасы еды. Дополнительное условие -- количество программистов, еды и официантов должно быть параметризируемое. [Это усложнение классической задачи, про которую можно почитать тут](https://en.wikipedia.org/wiki/Dining_philosophers_problem) -Необходимо обеспечить корректное выполнение программы, чтобы избежать состояний взаимной блокировки и гарантировать, что каждый программист получит возможность поесть. +Необходимо обеспечить корректное выполнение программы, чтобы избежать состояний взаимной блокировки и гарантировать, что +каждый программист получит возможность поесть. # Обязательное условие: + * Использование системы сборки Gradle * Код должен быть отлажен и протестирован -# Дедлайн 08.10.2025 23:59 \ No newline at end of file +# Дедлайн 08.10.2025 23:59 + +# Решения + +## 1. Упорядочивание блокировок + +Решение заключается в том, что последний студент захватывает ложку в другом порядке. +Если все сначала захватывают левую ложку, а затем правую, то последний студент захватывает правую ложку, а затем левую. + +Решение расположено в пакете orderedlocks. + +Такое решение **не** обеспечивает fairness (все студенты поедят одинаково). + +Ниже рассмотрены различные варианты вокруг этого подхода. + +### 1.1. Добавление различных задержек + +Решение дает fairness в случае, когда работа **вне** критической секции дольше работы внутри критической секции. + +Это обосновывается тем, что студенты не конкурируют за захват ресурса. + +```java + var config = Config.builder() + .NUMBER_OF_STUDENTS(7) + .NUMBER_OF_SOUP(10_000) + .NUMBER_OF_WAITERS(2) + .TIME_TO_EAT_SOUP_MS(Eat Delay) + .TIME_TO_SPEAK_MS(Speak Delay) + .FAIR_IF_POSSIBLE(false) + .build(); +``` + +| Speak Delay | Eat Delay | Fairness Array | +|-------------|-----------|--------------------------------------------| +| 0 | 0 | [219, 403, 771, 1505, 1778, 5144, 180] | +| 0 | 1 | [574, 858, 1062, 1711, 1862, 3430, 503] | +| 1 | 0 | [1428, 1430, 1428, 1427, 1428, 1430, 1429] | +| 1 | 1 | [1292, 1457, 1481, 1490, 1492, 1496, 1292] | +| 2 | 1 | [1428, 1428, 1429, 1429, 1429, 1429, 1428] | + +### 1.2. Использование параметра `fair` в Java API + +У классов ReentrantLock и ArrayBlockingQueue есть параметр `fair`, который позволяет гарантировать "честность" при +захвате ресурсов, в моем случае это **почему-то** полностью не решает проблему, но значительно улучшает честность. + +При этом стоит помнить, что fairness небесплатная, для демонстрации этого эффекта был реализован JMH +`OrderedLocksTests`, для запуска следует добавить gradlew права на исполнение (`sudo chmod +x ./gradlew`) и вызвать +`./gradlew jmh`. В тесте использовались задержки равные 0. Результат теста будет в `./build/results/jmh/results.txt`. + +Результаты на моей машине (MAC M2 MAX 32GB): + +```text +Benchmark Mode Cnt Score Error Units +OrderedLocksTests.zeroDelayFairTest avgt 5 171.625 ± 13.561 ms/op +OrderedLocksTests.zeroDelayNonFairTest avgt 5 105.592 ± 27.446 ms/op +``` + +| fair | fairness array | +|-------|--------------------------------------------| +| false | [227, 433, 724, 1483, 1690, 5291, 152] | +| true | [1179, 1301, 1249, 1333, 1434, 2322, 1182] | + +## 2. Использование арбитра + +Решение заключается в использовании специального класса-арбитра, который следит за числом одновременно едящих студентов, +это число не должно превышать число студентов - 1. Этот инвариант реализуется через семафор. + +Решение находится в пакете semaphore. + +Аналогично `ReentrantLock` `Semaphore` имеет параметр `fair`, который вынесен в config. + +Это решение показывает более высокую честность, чем решение с упорядочиванием блокировок, даже при `fair = false`. + +| fair | fairness array | +|-------|--------------------------------------------| +| false | [1430, 1424, 1446, 1437, 1454, 1436, 1373] | +| true | [1428, 1429, 1428, 1429, 1429, 1430, 1427] | + +При этом fairness для семафора тоже не бесплатная, для демонстрации этого эффекта был реализован JMH тест +`SemaphoreLocksTests`, запуск аналогичен решению с упорядочиванием блокировок. + +Полные результаты: + +```text +Benchmark Mode Cnt Score Error Units +o.l.orderedlocks.OrderedLocksTests.zeroDelayFairTest avgt 5 170.558 ± 26.265 ms/op +o.l.orderedlocks.OrderedLocksTests.zeroDelayNonFairTest avgt 5 106.313 ± 9.687 ms/op +o.l.semaphore.SemaphoreLocksTests.zeroDelayFairTest avgt 5 157.910 ± 62.556 ms/op +o.l.semaphore.SemaphoreLocksTests.zeroDelayNonFairTest avgt 5 117.113 ± 25.536 ms/op +``` diff --git a/build.gradle.kts b/build.gradle.kts index bda0d97..b7b5acc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("java") + id("me.champeau.jmh") version "0.7.3" } group = "org.labs" @@ -10,6 +11,12 @@ 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.30") + annotationProcessor("org.projectlombok:lombok:1.18.30") + testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") } diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/jmh/java/org/labs/orderedlocks/OrderedLocksTests.java b/src/jmh/java/org/labs/orderedlocks/OrderedLocksTests.java new file mode 100644 index 0000000..1ddd792 --- /dev/null +++ b/src/jmh/java/org/labs/orderedlocks/OrderedLocksTests.java @@ -0,0 +1,52 @@ +package org.labs.orderedlocks; + +import java.util.concurrent.TimeUnit; +import org.labs.common.Config; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@Fork(1) +@State(Scope.Benchmark) +@Warmup(iterations = 2) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class OrderedLocksTests { + + private final Config zeroDelayNonFairTest = Config.builder() + .NUMBER_OF_STUDENTS(7) + .NUMBER_OF_SOUP(10_000) + .NUMBER_OF_WAITERS(2) + .TIME_TO_EAT_SOUP_MS(0) + .TIME_TO_SPEAK_MS(0) + .FAIR_IF_POSSIBLE(false) + .build(); + + private final Config zeroDelayFairTest = Config.builder() + .NUMBER_OF_STUDENTS(7) + .NUMBER_OF_SOUP(10_000) + .NUMBER_OF_WAITERS(2) + .TIME_TO_EAT_SOUP_MS(0) + .TIME_TO_SPEAK_MS(0) + .FAIR_IF_POSSIBLE(true) + .build(); + + @Benchmark + public void zeroDelayNonFairTest() throws InterruptedException { + var diningStudents = new DiningStudentsSimulation(zeroDelayNonFairTest); + diningStudents.simulate(); + } + + @Benchmark + public void zeroDelayFairTest() throws InterruptedException { + var diningStudents = new DiningStudentsSimulation(zeroDelayFairTest); + diningStudents.simulate(); + } +} diff --git a/src/jmh/java/org/labs/semaphore/SemaphoreLocksTests.java b/src/jmh/java/org/labs/semaphore/SemaphoreLocksTests.java new file mode 100644 index 0000000..a7e73e2 --- /dev/null +++ b/src/jmh/java/org/labs/semaphore/SemaphoreLocksTests.java @@ -0,0 +1,52 @@ +package org.labs.semaphore; + +import java.util.concurrent.TimeUnit; +import org.labs.common.Config; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@Fork(1) +@State(Scope.Benchmark) +@Warmup(iterations = 2) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class SemaphoreLocksTests { + + private final Config zeroDelayNonFairTest = Config.builder() + .NUMBER_OF_STUDENTS(7) + .NUMBER_OF_SOUP(10_000) + .NUMBER_OF_WAITERS(2) + .TIME_TO_EAT_SOUP_MS(0) + .TIME_TO_SPEAK_MS(0) + .FAIR_IF_POSSIBLE(false) + .build(); + + private final Config zeroDelayFairTest = Config.builder() + .NUMBER_OF_STUDENTS(7) + .NUMBER_OF_SOUP(10_000) + .NUMBER_OF_WAITERS(2) + .TIME_TO_EAT_SOUP_MS(0) + .TIME_TO_SPEAK_MS(0) + .FAIR_IF_POSSIBLE(true) + .build(); + + @Benchmark + public void zeroDelayNonFairTest() throws InterruptedException { + var diningStudents = new DiningStudentsSimulation(zeroDelayNonFairTest); + diningStudents.simulate(); + } + + @Benchmark + public void zeroDelayFairTest() throws InterruptedException { + var diningStudents = new DiningStudentsSimulation(zeroDelayFairTest); + diningStudents.simulate(); + } +} 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/common/Config.java b/src/main/java/org/labs/common/Config.java new file mode 100644 index 0000000..1cf3883 --- /dev/null +++ b/src/main/java/org/labs/common/Config.java @@ -0,0 +1,18 @@ +package org.labs.common; + +import lombok.Builder; +import lombok.RequiredArgsConstructor; + +@Builder +@RequiredArgsConstructor +public class Config { + + public final int NUMBER_OF_STUDENTS; + public final int NUMBER_OF_SOUP; + public final int NUMBER_OF_WAITERS; + + public final long TIME_TO_EAT_SOUP_MS; + public final long TIME_TO_SPEAK_MS; + + public final boolean FAIR_IF_POSSIBLE; +} diff --git a/src/main/java/org/labs/common/Kitchen.java b/src/main/java/org/labs/common/Kitchen.java new file mode 100644 index 0000000..c95d6b1 --- /dev/null +++ b/src/main/java/org/labs/common/Kitchen.java @@ -0,0 +1,37 @@ +package org.labs.common; + +import java.util.concurrent.atomic.AtomicInteger; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Kitchen { + + private final AtomicInteger soupCount; + + public enum SoupOrderStatus { + OK, + OUT_OF_SOUP, + } + + public Kitchen(int initialSoupCount) { + soupCount = new AtomicInteger(initialSoupCount); + } + + public SoupOrderStatus getSoup() { + Integer currentSoupCount; + + do { + currentSoupCount = soupCount.get(); + + if (currentSoupCount.equals(0)) { + log.info("Kitchen is out of soup"); + return SoupOrderStatus.OUT_OF_SOUP; + } + } while (!soupCount.compareAndSet(currentSoupCount, currentSoupCount - 1)); + + if (currentSoupCount % 10_000 == 0) { + log.debug("Kitchen soup count {}", currentSoupCount); + } + return SoupOrderStatus.OK; + } +} diff --git a/src/main/java/org/labs/common/Spoon.java b/src/main/java/org/labs/common/Spoon.java new file mode 100644 index 0000000..88db7ee --- /dev/null +++ b/src/main/java/org/labs/common/Spoon.java @@ -0,0 +1,28 @@ +package org.labs.common; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import lombok.Getter; + +public class Spoon { + @Getter + private final Integer id; + private final Lock lock; + + public Spoon(int id, boolean fairness) { + this.id = id; + lock = new ReentrantLock(fairness); + } + + public void lock() { + lock.lock(); + } + + public void unlock() { + lock.unlock(); + } + + public boolean tryLock() { + return lock.tryLock(); + } +} diff --git a/src/main/java/org/labs/common/Statistic.java b/src/main/java/org/labs/common/Statistic.java new file mode 100644 index 0000000..60ce266 --- /dev/null +++ b/src/main/java/org/labs/common/Statistic.java @@ -0,0 +1,57 @@ +package org.labs.common; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Statistic { + + private final List studentStatistic; + private final List waiterStatistic; + + public Statistic(Integer studentsCount, Integer waitersCount) { + this.studentStatistic = new ArrayList<>(studentsCount); + this.waiterStatistic = new ArrayList<>(waitersCount); + + for (int i = 0; i < studentsCount; ++i) { + this.studentStatistic.add(new AtomicInteger(0)); + } + + for (int i = 0; i < waitersCount; ++i) { + this.waiterStatistic.add(new AtomicInteger(0)); + } + } + + public void addStudentStatistic(int studentId) { + studentStatistic.get(studentId).incrementAndGet(); + } + + public void addWaiterStatistic(int waiterId) { + waiterStatistic.get(waiterId).incrementAndGet(); + } + + public void printStatistic() { + StringBuilder sb = new StringBuilder("Statistic:\nStudents: ["); + + for (int i = 0; i < studentStatistic.size(); ++i) { + sb.append(studentStatistic.get(i).get()); + if (i != studentStatistic.size() - 1) { + sb.append(", "); + } + } + + sb.append("]\nWaiters: ["); + + for (int i = 0; i < waiterStatistic.size(); ++i) { + sb.append(waiterStatistic.get(i).get()); + if (i != waiterStatistic.size() - 1) { + sb.append(", "); + } + } + sb.append("]"); + + log.info(sb.toString()); + } +} diff --git a/src/main/java/org/labs/orderedlocks/DiningStudentsSimulation.java b/src/main/java/org/labs/orderedlocks/DiningStudentsSimulation.java new file mode 100644 index 0000000..a4e14c2 --- /dev/null +++ b/src/main/java/org/labs/orderedlocks/DiningStudentsSimulation.java @@ -0,0 +1,125 @@ +package org.labs.orderedlocks; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.labs.common.Config; +import org.labs.common.Kitchen; +import org.labs.common.Kitchen.SoupOrderStatus; +import org.labs.common.Spoon; +import org.labs.common.Statistic; + +@Slf4j +public class DiningStudentsSimulation { + + public static void main(String[] args) throws InterruptedException { + var config = Config.builder() + .NUMBER_OF_STUDENTS(7) + .NUMBER_OF_SOUP(1_000_000) + .NUMBER_OF_WAITERS(2) + .TIME_TO_EAT_SOUP_MS(0) + .TIME_TO_SPEAK_MS(0) + .FAIR_IF_POSSIBLE(false) + .build(); + var diningStudents = new DiningStudentsSimulation(config); + diningStudents.simulate(); + } + + private final Config config; + + public DiningStudentsSimulation(Config config) { + this.config = config; + } + + public void simulate() throws InterruptedException { + var statistic = new Statistic(config.NUMBER_OF_STUDENTS, config.NUMBER_OF_WAITERS); + + List spoons = createSpoons(config.NUMBER_OF_STUDENTS); + + Kitchen kitchen = new Kitchen(config.NUMBER_OF_SOUP); + + BlockingQueue> orders = new ArrayBlockingQueue<>( + config.NUMBER_OF_SOUP + config.NUMBER_OF_STUDENTS + 1, + config.FAIR_IF_POSSIBLE + ); + + List waiters = createAndStartWaiters(config.NUMBER_OF_WAITERS, orders, kitchen, statistic); + + var startTime = System.nanoTime(); + List students = createAndStartStudents(config.NUMBER_OF_STUDENTS, spoons, statistic, orders); + + students.forEach((thread -> { + try { + thread.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }) + ); + var endTime = System.nanoTime(); + + for (var waiter : waiters) { + waiter.interrupt(); + waiter.join(); + } + + statistic.printStatistic(); + log.info("Time: {} ms", TimeUnit.NANOSECONDS.toMillis(endTime - startTime)); + } + + private List createSpoons(int numberOfSpoons) { + List spoons = new ArrayList<>(numberOfSpoons); + for (int i = 0; i < config.NUMBER_OF_STUDENTS; ++i) { + spoons.add(new Spoon(i, config.FAIR_IF_POSSIBLE)); + } + return spoons; + } + + private List createAndStartWaiters( + int numberOfWaiters, + BlockingQueue> orders, + Kitchen kitchen, + Statistic statistic + ) { + List waiters = new ArrayList<>(numberOfWaiters); + for (int i = 0; i < numberOfWaiters; ++i) { + var waiterRunnable = new Waiter(i, orders, kitchen, statistic); + var waiterThread = new Thread(waiterRunnable); + waiterThread.start(); + waiters.add(waiterThread); + } + return waiters; + } + + private List createAndStartStudents( + int numberOfStudents, + List spoons, + Statistic statistic, + BlockingQueue> orders + ) { + List students = new ArrayList<>(numberOfStudents); + for (int i = 0; i < numberOfStudents; ++i) { + // all students except for the last should take left spoon first + int firstSpoonId = i != config.NUMBER_OF_STUDENTS - 1 ? + i == 0 ? config.NUMBER_OF_STUDENTS - 1 : i - 1 : i; + int secondSpoonId = i != config.NUMBER_OF_STUDENTS - 1 ? i : i - 1; + var studentRunnable = new Student( + i, + statistic, + spoons.get(firstSpoonId), + spoons.get(secondSpoonId), + orders, + config.TIME_TO_SPEAK_MS, + config.TIME_TO_EAT_SOUP_MS + ); + var studentThread = new Thread(studentRunnable); + studentThread.start(); + students.add(studentThread); + } + return students; + } +} diff --git a/src/main/java/org/labs/orderedlocks/Student.java b/src/main/java/org/labs/orderedlocks/Student.java new file mode 100644 index 0000000..6b6434d --- /dev/null +++ b/src/main/java/org/labs/orderedlocks/Student.java @@ -0,0 +1,74 @@ +package org.labs.orderedlocks; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.labs.common.Kitchen.SoupOrderStatus; +import org.labs.common.Spoon; +import org.labs.common.Statistic; + +@Slf4j +@RequiredArgsConstructor +public class Student implements Runnable { + + private final Integer id; + private final Statistic statistic; + private final Spoon firstSpoon; + private final Spoon secondSpoon; + private final BlockingQueue> orders; + + private final long speakTimeMs; + private final long eatTimeMs; + + @Override + public void run() { + try { + while (true) { + try { + speak(); + + firstSpoon.lock(); + try { + log.debug("Student {} took spoon with id: {}", id, firstSpoon.getId()); + secondSpoon.lock(); + try { + log.debug("Student {} took spoon with id: {}", id, secondSpoon.getId()); + + CompletableFuture order = new CompletableFuture<>(); + orders.put(order); + + var orderStatus = order.get(); + + if (orderStatus == SoupOrderStatus.OUT_OF_SOUP) { + log.info("no more food for {}, leaving restaurant", id); + return; + } + + Thread.sleep(eatTimeMs); + statistic.addStudentStatistic(id); + } finally { + secondSpoon.unlock(); + log.debug("Student {} put down spoon with id: {}", id, secondSpoon.getId()); + } + } finally { + firstSpoon.unlock(); + log.debug("Student {} put down spoon with id: {}", id, firstSpoon.getId()); + } + } catch (ExecutionException e) { + // should not happen + log.error("Student {} received error, stopping", id, e); + return; + } + } + } catch (InterruptedException e) { + log.warn("Student {} was interrupted", id); + Thread.currentThread().interrupt(); + } + } + + private void speak() throws InterruptedException { + Thread.sleep(speakTimeMs); + } +} diff --git a/src/main/java/org/labs/orderedlocks/Waiter.java b/src/main/java/org/labs/orderedlocks/Waiter.java new file mode 100644 index 0000000..77aa873 --- /dev/null +++ b/src/main/java/org/labs/orderedlocks/Waiter.java @@ -0,0 +1,50 @@ +package org.labs.orderedlocks; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; +import org.labs.common.Kitchen; +import org.labs.common.Statistic; +import org.labs.common.Kitchen.SoupOrderStatus; + +@Slf4j +public class Waiter implements Runnable { + + private final Integer id; + private final BlockingQueue> orders; + private final Kitchen kitchen; + private final Statistic statistic; + + public Waiter(Integer id, BlockingQueue> orders, Kitchen kitchen, Statistic statistic) { + this.id = id; + this.orders = orders; + this.kitchen = kitchen; + this.statistic = statistic; + } + + @Override + public void run() { + try { + while (true) { + CompletableFuture order = null; + try { + order = orders.take(); + SoupOrderStatus orderStatus = kitchen.getSoup(); + + order.complete(orderStatus); + + if (orderStatus != SoupOrderStatus.OUT_OF_SOUP) { + statistic.addWaiterStatistic(id); + } + } finally { + if (order != null) { + order.complete(SoupOrderStatus.OUT_OF_SOUP); + } + } + } + } catch (InterruptedException e) { + log.info("Waiter {} was interrupted", id); + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/java/org/labs/semaphore/DiningStudentsSimulation.java b/src/main/java/org/labs/semaphore/DiningStudentsSimulation.java new file mode 100644 index 0000000..7bdf5ff --- /dev/null +++ b/src/main/java/org/labs/semaphore/DiningStudentsSimulation.java @@ -0,0 +1,126 @@ +package org.labs.semaphore; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.labs.common.Config; +import org.labs.common.Kitchen; +import org.labs.common.Kitchen.SoupOrderStatus; +import org.labs.common.Spoon; +import org.labs.common.Statistic; + +@Slf4j +public class DiningStudentsSimulation { + + public static void main(String[] args) throws InterruptedException { + var config = Config.builder() + .NUMBER_OF_STUDENTS(7) + .NUMBER_OF_SOUP(1_000_0) + .NUMBER_OF_WAITERS(2) + .TIME_TO_EAT_SOUP_MS(0) + .TIME_TO_SPEAK_MS(0) + .FAIR_IF_POSSIBLE(true) + .build(); + var diningStudents = new DiningStudentsSimulation(config); + diningStudents.simulate(); + } + + private final Config config; + + public DiningStudentsSimulation(Config config) { + this.config = config; + } + + public void simulate() throws InterruptedException { + var statistic = new Statistic(config.NUMBER_OF_STUDENTS, config.NUMBER_OF_WAITERS); + + List spoons = createSpoons(config.NUMBER_OF_STUDENTS); + var arbiter = new SpoonArbiter(config.NUMBER_OF_STUDENTS, config.FAIR_IF_POSSIBLE); + + Kitchen kitchen = new Kitchen(config.NUMBER_OF_SOUP); + + BlockingQueue> orders = new ArrayBlockingQueue<>( + config.NUMBER_OF_SOUP + config.NUMBER_OF_STUDENTS + 1, + config.FAIR_IF_POSSIBLE + ); + + List waiters = createAndStartWaiters(config.NUMBER_OF_WAITERS, orders, kitchen, statistic); + + var startTime = System.nanoTime(); + List students = createAndStartStudents(config.NUMBER_OF_STUDENTS, spoons, statistic, arbiter, orders); + + students.forEach((thread -> { + try { + thread.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }) + ); + var endTime = System.nanoTime(); + + for (var waiter : waiters) { + waiter.interrupt(); + waiter.join(); + } + + statistic.printStatistic(); + log.info("Time: {} ms", TimeUnit.NANOSECONDS.toMillis(endTime - startTime)); + } + + private List createSpoons(int numberOfSpoons) { + List spoons = new ArrayList<>(numberOfSpoons); + for (int i = 0; i < config.NUMBER_OF_STUDENTS; ++i) { + spoons.add(new Spoon(i, config.FAIR_IF_POSSIBLE)); + } + return spoons; + } + + private List createAndStartWaiters( + int numberOfWaiters, + BlockingQueue> orders, + Kitchen kitchen, + Statistic statistic + ) { + List waiters = new ArrayList<>(numberOfWaiters); + for (int i = 0; i < numberOfWaiters; ++i) { + var waiterRunnable = new Waiter(i, orders, kitchen, statistic); + var waiterThread = new Thread(waiterRunnable); + waiterThread.start(); + waiters.add(waiterThread); + } + return waiters; + } + + private List createAndStartStudents( + int numberOfStudents, + List spoons, + Statistic statistic, + SpoonArbiter arbiter, + BlockingQueue> orders + ) { + List students = new ArrayList<>(numberOfStudents); + for (int i = 0; i < numberOfStudents; ++i) { + int firstSpoonId = i == 0 ? numberOfStudents - 1 : i - 1; + int secondSpoonId = i; + var studentRunnable = new Student( + i, + statistic, + spoons.get(firstSpoonId), + spoons.get(secondSpoonId), + arbiter, + orders, + config.TIME_TO_SPEAK_MS, + config.TIME_TO_EAT_SOUP_MS + ); + var studentThread = new Thread(studentRunnable); + studentThread.start(); + students.add(studentThread); + } + return students; + } +} diff --git a/src/main/java/org/labs/semaphore/SpoonArbiter.java b/src/main/java/org/labs/semaphore/SpoonArbiter.java new file mode 100644 index 0000000..fe12edc --- /dev/null +++ b/src/main/java/org/labs/semaphore/SpoonArbiter.java @@ -0,0 +1,20 @@ +package org.labs.semaphore; + +import java.util.concurrent.Semaphore; + +public class SpoonArbiter { + + private final Semaphore semaphore; + + public SpoonArbiter(int numberOfStudents, boolean fairness) { + this.semaphore = new Semaphore(numberOfStudents - 1, fairness); + } + + public void acquire() throws InterruptedException { + semaphore.acquire(); + } + + public void release() { + semaphore.release(); + } +} diff --git a/src/main/java/org/labs/semaphore/Student.java b/src/main/java/org/labs/semaphore/Student.java new file mode 100644 index 0000000..2149330 --- /dev/null +++ b/src/main/java/org/labs/semaphore/Student.java @@ -0,0 +1,80 @@ +package org.labs.semaphore; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.labs.common.Kitchen.SoupOrderStatus; +import org.labs.common.Spoon; +import org.labs.common.Statistic; + +@Slf4j +@RequiredArgsConstructor +public class Student implements Runnable { + + private final Integer id; + private final Statistic statistic; + private final Spoon firstSpoon; + private final Spoon secondSpoon; + private final SpoonArbiter arbiter; + private final BlockingQueue> orders; + + private final long speakTimeMs; + private final long eatTimeMs; + + @Override + public void run() { + try { + while (true) { + try { + speak(); + + arbiter.acquire(); + try { + firstSpoon.lock(); + try { + log.debug("Student {} took spoon with id: {}", id, firstSpoon.getId()); + secondSpoon.lock(); + try { + log.debug("Student {} took spoon with id: {}", id, secondSpoon.getId()); + + CompletableFuture order = new CompletableFuture<>(); + orders.put(order); + + var orderStatus = order.get(); + + if (orderStatus == SoupOrderStatus.OUT_OF_SOUP) { + log.info("no more food for {}, leaving restaurant", id); + return; + } + + Thread.sleep(eatTimeMs); + statistic.addStudentStatistic(id); + } finally { + secondSpoon.unlock(); + log.debug("Student {} put down spoon with id: {}", id, secondSpoon.getId()); + } + } finally { + firstSpoon.unlock(); + log.debug("Student {} put down spoon with id: {}", id, firstSpoon.getId()); + } + } finally { + arbiter.release(); + } + } catch (ExecutionException e) { + // should not happen + log.error("Student {} received error, stopping", id, e); + return; + } + } + } catch (InterruptedException e) { + log.warn("Student {} was interrupted", id); + Thread.currentThread().interrupt(); + } + } + + private void speak() throws InterruptedException { + Thread.sleep(speakTimeMs); + } +} diff --git a/src/main/java/org/labs/semaphore/Waiter.java b/src/main/java/org/labs/semaphore/Waiter.java new file mode 100644 index 0000000..a0ebe54 --- /dev/null +++ b/src/main/java/org/labs/semaphore/Waiter.java @@ -0,0 +1,45 @@ +package org.labs.semaphore; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.labs.common.Kitchen; +import org.labs.common.Kitchen.SoupOrderStatus; +import org.labs.common.Statistic; + +@Slf4j +@RequiredArgsConstructor +public class Waiter implements Runnable { + + private final Integer id; + private final BlockingQueue> orders; + private final Kitchen kitchen; + private final Statistic statistic; + + @Override + public void run() { + try { + while (true) { + CompletableFuture order = null; + try { + order = orders.take(); + SoupOrderStatus orderStatus = kitchen.getSoup(); + + order.complete(orderStatus); + + if (orderStatus != SoupOrderStatus.OUT_OF_SOUP) { + statistic.addWaiterStatistic(id); + } + } finally { + if (order != null) { + order.complete(SoupOrderStatus.OUT_OF_SOUP); + } + } + } + } catch (InterruptedException e) { + log.info("Waiter {} was interrupted", id); + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..aedad0c --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + +