From 8eef89d526f8acb2fc1d69734b2b3acb7ecf7616 Mon Sep 17 00:00:00 2001 From: egorsv21 Date: Tue, 23 Dec 2025 20:20:58 +0300 Subject: [PATCH] Implemented lab 1 --- .gitignore | 4 +- README.md | 19 +- build.gradle.kts | 3 +- gradlew | 0 results.txt | 29 +++ src/main/java/org/labs/Cafe.java | 42 +++++ src/main/java/org/labs/CafeConfig.java | 23 +++ .../java/org/labs/LimitedSpoonService.java | 40 +++++ src/main/java/org/labs/Main.java | 167 +++++++++++++++++- .../java/org/labs/OrderedSpoonService.java | 51 ++++++ .../org/labs/PrioritizedWaiterService.java | 87 +++++++++ src/main/java/org/labs/Programmer.java | 62 +++++++ .../java/org/labs/SimpleWaiterService.java | 56 ++++++ src/main/java/org/labs/SpoonService.java | 8 + src/main/java/org/labs/WaiterService.java | 11 ++ 15 files changed, 597 insertions(+), 5 deletions(-) mode change 100644 => 100755 gradlew create mode 100644 results.txt create mode 100644 src/main/java/org/labs/Cafe.java create mode 100644 src/main/java/org/labs/CafeConfig.java create mode 100644 src/main/java/org/labs/LimitedSpoonService.java create mode 100644 src/main/java/org/labs/OrderedSpoonService.java create mode 100644 src/main/java/org/labs/PrioritizedWaiterService.java create mode 100644 src/main/java/org/labs/Programmer.java create mode 100644 src/main/java/org/labs/SimpleWaiterService.java create mode 100644 src/main/java/org/labs/SpoonService.java create mode 100644 src/main/java/org/labs/WaiterService.java diff --git a/.gitignore b/.gitignore index b63da45..a98c271 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 @@ -39,4 +40,5 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store +/.idea/ diff --git a/README.md b/README.md index e974d43..561f0f8 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,21 @@ * Использование системы сборки Gradle * Код должен быть отлажен и протестирован -# Дедлайн 08.10.2025 23:59 \ No newline at end of file +# Дедлайн 08.10.2025 23:59 + +Решение: + +Для предотвращения deadlockа пронумеруем ложки от 0 до n - 1. +Сначала программист будет дожидаться ложки с меньшим номером, затем с большим. + +Официанты будут реализованы как пул потоков в ExecutorService. +При запросе еды программист блокируется, пока ему ее не принесут. +Полученная future вернет true, если еда была выдана, и false, если она закончилась. +При false поток программиста завершает работу. + +Step 1 - без приоритетов + + +Step 2 - с приоритетами: + +Когда программист поел, у его соседей повышается приоритет, а свой сбрасывается. Если сосед с высшим приоритетом ждет ложку, текущий программист не имеет права ее брать. diff --git a/build.gradle.kts b/build.gradle.kts index bda0d97..10c0b91 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("java") + id("io.freefair.lombok") version "9.1.0" } group = "org.labs" @@ -16,4 +17,4 @@ dependencies { tasks.test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/results.txt b/results.txt new file mode 100644 index 0000000..3aa63ec --- /dev/null +++ b/results.txt @@ -0,0 +1,29 @@ +OrderedSpoonService + SimpleWaiterService +Total time: 23217 ms +Delta: 55.76% +[8.16, 9.14, 9.35, 9.65, 10.04, 10.34, 10.9, 11.54, 12.71, 8.17] + +OrderedSpoonService + SimpleWaiterService (discuss time > eat time) +Total time: 12609 ms +Delta: 4.57% +[9.85, 9.97, 9.88, 9.85, 9.96, 9.94, 10.22, 10.14, 10.3, 9.89] + +LimitedSpoonService + SimpleWaiterService +Total time: 72404 ms +Delta: 0.20% +[10.0, 10.0, 9.99, 9.99, 10.0, 10.01, 10.01, 10.0, 10.0, 10.0] + +OrderedSpoonService + PrioritizedWaiterService (10%) +Total time: 23475 ms +Delta: 59.88% +[8.11, 9.01, 9.31, 9.73, 9.94, 10.38, 10.92, 11.55, 12.95, 8.1] + +OrderedSpoonService + PrioritizedWaiterService (1%) +Total time: 27421 ms +Delta: 10.88% +[9.28, 9.77, 9.95, 10.27, 10.29, 10.29, 10.29, 10.29, 10.29, 9.28] + +LimitedSpoonService + PrioritizedWaiterService (1%) +Total time: 79718 ms +Delta: 0.20% +[10.0, 10.0, 10.0, 9.99, 10.01, 10.01, 10.0, 10.0, 10.0, 9.99] diff --git a/src/main/java/org/labs/Cafe.java b/src/main/java/org/labs/Cafe.java new file mode 100644 index 0000000..5f70e71 --- /dev/null +++ b/src/main/java/org/labs/Cafe.java @@ -0,0 +1,42 @@ +package org.labs; + +import java.util.List; +import java.util.stream.IntStream; + +public class Cafe { + + private final WaiterService waiterService; + private final List programmers; + private final List programmerThreads; + + public Cafe(CafeConfig config, WaiterService waiterService, SpoonService spoonService) { + this.waiterService = waiterService; + programmers = IntStream.range(0, config.getProgrammersNumber()) + .mapToObj(id -> new Programmer(waiterService, spoonService, id, config)) + .toList(); + + programmerThreads = programmers.stream().map(Thread::new).toList(); + } + + public void start() { + programmerThreads.forEach(Thread::start); + } + + public void join() { + for (var p : programmerThreads) { + if (Thread.currentThread().isInterrupted()) { + p.interrupt(); + } + try { + p.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + waiterService.shutdown(); + } + + public List getFoodDistribution() { + return programmers.stream().map(Programmer::getEatenFood).toList(); + } +} diff --git a/src/main/java/org/labs/CafeConfig.java b/src/main/java/org/labs/CafeConfig.java new file mode 100644 index 0000000..27703b9 --- /dev/null +++ b/src/main/java/org/labs/CafeConfig.java @@ -0,0 +1,23 @@ +package org.labs; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class CafeConfig { + private final int programmersNumber; + private final int waitersNumber; + private final int totalFood; + private final int spoonCount; + @Builder.Default + private final int minDiscussTimeInMillis = 1; + @Builder.Default + private final int maxDiscussTimeInMillis = 5; + @Builder.Default + private final int minEatTimeInMillis = 5; + @Builder.Default + private final int maxEatTimeInMillis = 10; + private final Integer deltaPercentage; // Optional, for PrioritizedWaiterService +} + diff --git a/src/main/java/org/labs/LimitedSpoonService.java b/src/main/java/org/labs/LimitedSpoonService.java new file mode 100644 index 0000000..5fa7909 --- /dev/null +++ b/src/main/java/org/labs/LimitedSpoonService.java @@ -0,0 +1,40 @@ +package org.labs; + +import java.util.List; +import java.util.concurrent.Semaphore; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.IntStream; + +/** + * Позволяет взять левую ложку n - 1 раз. + * Предотвращает ситуацию, когда все n программистов взяли левую ложку. + */ +public class LimitedSpoonService implements SpoonService { + + private final int spoonCount; + private final List spoonLocks; + private final Semaphore semaphore; + + public LimitedSpoonService(int spoonCount) { + this.spoonCount = spoonCount; + this.spoonLocks = IntStream.range(0, spoonCount) + .mapToObj(i -> new ReentrantLock()) + .toList(); + this.semaphore = new Semaphore(spoonCount - 1, true); + } + + @Override + public void acquireSpoons(int id) throws InterruptedException { + semaphore.acquire(); + spoonLocks.get(id).lock(); + spoonLocks.get((id + 1) % spoonCount).lock(); + } + + @Override + public void releaseSpoons(int id) { + spoonLocks.get((id + 1) % spoonCount).unlock(); + spoonLocks.get(id).unlock(); + semaphore.release(); + } + +} diff --git a/src/main/java/org/labs/Main.java b/src/main/java/org/labs/Main.java index 9917247..d7d162f 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -1,7 +1,170 @@ package org.labs; +import java.io.FileNotFoundException; +import java.io.PrintWriter; + public class Main { + + // SpoonService: + // OrderedSpoonService + // LimitedSpoonService + // WaiterService: + // SimpleWaiterService + // PrioritizedWaiterService + public static void main(String[] args) { - System.out.println("Hello, World!"); + PrintWriter fileOut; + try { + fileOut = new PrintWriter("results.txt"); + } catch (FileNotFoundException e) { + System.err.println("Failed to create results.txt: " + e.getMessage()); + return; + } + + fileOut.println("OrderedSpoonService + SimpleWaiterService"); + CafeConfig config = CafeConfig.builder() + .programmersNumber(10) + .waitersNumber(4) + .totalFood(10000) + .spoonCount(10) + .minDiscussTimeInMillis(1) + .maxDiscussTimeInMillis(5) + .minEatTimeInMillis(5) + .maxEatTimeInMillis(10) + .build(); + test( + fileOut, + config, + new OrderedSpoonService(config.getSpoonCount()), + new SimpleWaiterService(config.getWaitersNumber(), config.getTotalFood()) + ); + + + fileOut.println("\nOrderedSpoonService + SimpleWaiterService (discuss time > eat time)"); + config = CafeConfig.builder() + .programmersNumber(10) + .waitersNumber(4) + .totalFood(10000) + .spoonCount(10) + .minDiscussTimeInMillis(5) + .maxDiscussTimeInMillis(10) + .minEatTimeInMillis(1) + .maxEatTimeInMillis(5) + .build(); + test( + fileOut, + config, + new OrderedSpoonService(config.getSpoonCount()), + new SimpleWaiterService(config.getWaitersNumber(), config.getTotalFood()) + ); + + fileOut.println("\nLimitedSpoonService + SimpleWaiterService"); + config = CafeConfig.builder() + .programmersNumber(10) + .waitersNumber(4) + .totalFood(10000) + .spoonCount(10) + .minDiscussTimeInMillis(1) + .maxDiscussTimeInMillis(5) + .minEatTimeInMillis(5) + .maxEatTimeInMillis(10) + .build(); + test( + fileOut, + config, + new LimitedSpoonService(config.getSpoonCount()), + new SimpleWaiterService(config.getWaitersNumber(), config.getTotalFood()) + ); + + fileOut.println("\nOrderedSpoonService + PrioritizedWaiterService (10%)"); + config = CafeConfig.builder() + .programmersNumber(10) + .waitersNumber(4) + .totalFood(10000) + .spoonCount(10) + .minDiscussTimeInMillis(1) + .maxDiscussTimeInMillis(5) + .minEatTimeInMillis(5) + .maxEatTimeInMillis(10) + .deltaPercentage(10) + .build(); + test( + fileOut, + config, + new OrderedSpoonService(config.getSpoonCount()), + new PrioritizedWaiterService(config.getProgrammersNumber(), config.getWaitersNumber(), config.getTotalFood(), config.getDeltaPercentage()) + ); + + fileOut.println("\nOrderedSpoonService + PrioritizedWaiterService (1%)"); + System.out.println("OrderedSpoonService + PrioritizedWaiterService (1%)"); + config = CafeConfig.builder() + .programmersNumber(10) + .waitersNumber(4) + .totalFood(10000) + .spoonCount(10) + .minDiscussTimeInMillis(1) + .maxDiscussTimeInMillis(5) + .minEatTimeInMillis(5) + .maxEatTimeInMillis(10) + .deltaPercentage(1) + .build(); + test( + fileOut, + config, + new OrderedSpoonService(config.getSpoonCount()), + new PrioritizedWaiterService(config.getProgrammersNumber(), config.getWaitersNumber(), config.getTotalFood(), config.getDeltaPercentage()) + ); + + fileOut.println("\nLimitedSpoonService + PrioritizedWaiterService (1%)"); + config = CafeConfig.builder() + .programmersNumber(10) + .waitersNumber(4) + .totalFood(10000) + .spoonCount(10) + .minDiscussTimeInMillis(1) + .maxDiscussTimeInMillis(5) + .minEatTimeInMillis(5) + .maxEatTimeInMillis(10) + .deltaPercentage(1) + .build(); + test( + fileOut, + config, + new LimitedSpoonService(config.getSpoonCount()), + new PrioritizedWaiterService(config.getProgrammersNumber(), config.getWaitersNumber(), config.getTotalFood(), config.getDeltaPercentage()) + ); + + fileOut.close(); + } + + private static void test( + PrintWriter fileOut, + CafeConfig config, + SpoonService spoonService, + WaiterService waiterService + ) { + Cafe cafe = new Cafe(config, waiterService, spoonService); + + long startTime = System.currentTimeMillis(); + + cafe.start(); + cafe.join(); + + long endTime = System.currentTimeMillis(); + + long totalTime = endTime - startTime; + + var distribution = cafe.getFoodDistribution(); + var sum = distribution.stream().mapToInt(Integer::intValue).sum(); + var percentage = distribution.stream().map(value -> 100.0 * value / sum).toList(); + var minFood = distribution.stream().min(Integer::compare).get(); + var maxFood = distribution.stream().max(Integer::compare).get(); + var delta = 100.0 * (maxFood - minFood) / minFood; + + fileOut.println("Total time: " + totalTime + " ms"); + fileOut.printf("Delta: %.2f", delta); + fileOut.println("%"); + fileOut.println(percentage); + fileOut.flush(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/labs/OrderedSpoonService.java b/src/main/java/org/labs/OrderedSpoonService.java new file mode 100644 index 0000000..97350cc --- /dev/null +++ b/src/main/java/org/labs/OrderedSpoonService.java @@ -0,0 +1,51 @@ +package org.labs; + +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.IntStream; + +/** + * Ложки нумеруются. + * Все программисты берут ложку сначала с наименьшим номером, затем с наибольшим. + */ +public class OrderedSpoonService implements SpoonService { + + private final int spoonCount; + private final List spoonLocks; + + public OrderedSpoonService(int spoonCount) { + this.spoonCount = spoonCount; + this.spoonLocks = IntStream.range(0, spoonCount) + .mapToObj(i -> new ReentrantLock()) + .toList(); + } + + public void acquireSpoons(int id) { + int first = id; + int second = (id + 1) % spoonCount; + + if (first > second) { + int temp = first; + first = second; + second = temp; + } + + spoonLocks.get(first).lock(); + spoonLocks.get(second).lock(); + } + + public void releaseSpoons(int id) { + int first = id; + int second = (id + 1) % spoonCount; + + if (first > second) { + int temp = first; + first = second; + second = temp; + } + + spoonLocks.get(second).unlock(); + spoonLocks.get(first).unlock(); + } + +} diff --git a/src/main/java/org/labs/PrioritizedWaiterService.java b/src/main/java/org/labs/PrioritizedWaiterService.java new file mode 100644 index 0000000..b0eb9e2 --- /dev/null +++ b/src/main/java/org/labs/PrioritizedWaiterService.java @@ -0,0 +1,87 @@ +package org.labs; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.IntStream; + +/** + * У программистов есть кол-во запрошенной еды. + * Если программист съел больше самого голодного программиста на deltaPercentage, он ожидает. + * Важно, чтобы еда запрашивалась до ложек. + */ +public class PrioritizedWaiterService implements WaiterService { + private final ReentrantLock lock = new ReentrantLock(); + private final Condition condition = lock.newCondition(); + + private final Semaphore semaphore; + private final AtomicInteger totalFood; + + private final AtomicIntegerArray requestedFood; + + private final int deltaPercentage; + private final int initialFood; + + public PrioritizedWaiterService( + int programmersNumber, + int waitersNumber, + int totalFood, + int deltaPercentage + ) { + this.semaphore = new Semaphore(waitersNumber); + this.totalFood = new AtomicInteger(totalFood); + + this.requestedFood = new AtomicIntegerArray(programmersNumber); + this.initialFood = totalFood; + this.deltaPercentage = deltaPercentage; + } + + public Future getFood(int id) throws InterruptedException { + lock.lock(); + try { + while (requestedFood.get(id) - getApproximateMinimalFood() > deltaPercentage * initialFood / 100.0 + && totalFood.get() > 0) { + condition.await(); + } + + semaphore.acquire(); + var remainingFood = totalFood.decrementAndGet(); + semaphore.release(); + + if (remainingFood < 0) { + totalFood.set(0); + condition.signalAll(); + return CompletableFuture.completedFuture(false); + } + + requestedFood.set(id, requestedFood.get(id) + 1); + notice(remainingFood); + + condition.signalAll(); + return CompletableFuture.completedFuture(true); + } finally { + lock.unlock(); + } + } + + public void shutdown() { + } + + private int getApproximateMinimalFood() { + return IntStream.range(0, requestedFood.length()).map(requestedFood::get).min().orElse(0); + } + + private void notice(int remainingFood) { + int chunkNumber = 5; + int chunkSize = initialFood / chunkNumber; + if ((remainingFood + 1) % chunkSize == 0) { + int currentChunkNumber = chunkNumber - (remainingFood + 1) / chunkSize; + System.out.println(100 / chunkNumber * currentChunkNumber + "% completed"); + System.out.flush(); + } + } +} diff --git a/src/main/java/org/labs/Programmer.java b/src/main/java/org/labs/Programmer.java new file mode 100644 index 0000000..dca1660 --- /dev/null +++ b/src/main/java/org/labs/Programmer.java @@ -0,0 +1,62 @@ +package org.labs; + +import lombok.Getter; + +import java.util.Random; +import java.util.concurrent.ExecutionException; + +public class Programmer implements Runnable { + + private final WaiterService waiterService; + private final SpoonService spoonService; + private final CafeConfig config; + + private final Random random = new Random(); + + private final int id; + @Getter + private int eatenFood; + + public Programmer( + WaiterService waiterService, + SpoonService spoonService, + int id, + CafeConfig config + ) { + this.waiterService = waiterService; + this.spoonService = spoonService; + this.id = id; + this.config = config; + } + + @Override + public void run() { + try { + while (true) { + discuss(); + if (!waiterService.getFood(id).get()) { + break; + } + spoonService.acquireSpoons(id); + eat(); + spoonService.releaseSpoons(id); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private void discuss() throws InterruptedException { + var waitTime = random.nextInt(config.getMinDiscussTimeInMillis(), config.getMaxDiscussTimeInMillis()); + Thread.sleep(waitTime); + } + + private void eat() throws InterruptedException { + eatenFood++; + var eatTime = random.nextInt(config.getMinEatTimeInMillis(), config.getMaxEatTimeInMillis()); + Thread.sleep(eatTime); + } + +} diff --git a/src/main/java/org/labs/SimpleWaiterService.java b/src/main/java/org/labs/SimpleWaiterService.java new file mode 100644 index 0000000..b2a7dc7 --- /dev/null +++ b/src/main/java/org/labs/SimpleWaiterService.java @@ -0,0 +1,56 @@ +package org.labs; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; + +public class SimpleWaiterService implements WaiterService { + + private final ExecutorService waitersPool; + private final AtomicInteger totalFood; + + private final int initialFood; + + public SimpleWaiterService( + int waitersNumber, + int totalFood + ) { + this.waitersPool = Executors.newFixedThreadPool(waitersNumber); + this.initialFood = totalFood; + this.totalFood = new AtomicInteger(totalFood); + } + + public Future getFood(int id) { + return waitersPool.submit(new WaiterJob()); + } + + public void shutdown() { + waitersPool.shutdown(); + } + + public class WaiterJob implements Callable { + + @Override + public Boolean call() { + var remainingFood = totalFood.decrementAndGet(); + if (remainingFood < 0) { + totalFood.set(0); + return false; + } + notice(remainingFood); + return true; + } + + private void notice(int remainingFood) { + int chunkNumber = 5; + int chunkSize = initialFood / chunkNumber; + if ((remainingFood + 1) % chunkSize == 0) { + int currentChunkNumber = chunkNumber - (remainingFood + 1) / chunkSize; + System.out.println(100 / chunkNumber * currentChunkNumber + "% completed"); + System.out.flush(); + } + } + } +} diff --git a/src/main/java/org/labs/SpoonService.java b/src/main/java/org/labs/SpoonService.java new file mode 100644 index 0000000..752593f --- /dev/null +++ b/src/main/java/org/labs/SpoonService.java @@ -0,0 +1,8 @@ +package org.labs; + +public interface SpoonService { + + void acquireSpoons(int id) throws InterruptedException; + + void releaseSpoons(int id); +} diff --git a/src/main/java/org/labs/WaiterService.java b/src/main/java/org/labs/WaiterService.java new file mode 100644 index 0000000..abba5e9 --- /dev/null +++ b/src/main/java/org/labs/WaiterService.java @@ -0,0 +1,11 @@ +package org.labs; + +import java.util.concurrent.Future; + +public interface WaiterService { + + Future getFood(int id) throws InterruptedException; + + void shutdown(); + +}