diff --git a/.gitignore b/.gitignore index b63da45..f68d109 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,4 @@ -.gradle -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - ### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ -*.iws -*.iml -*.ipr out/ !**/src/main/**/out/ !**/src/test/**/out/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e908837 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM openjdk:24-jdk-slim + +WORKDIR /app + +COPY . . + +RUN chmod +x gradlew + +RUN ./gradlew build --no-daemon \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..857c713 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +run_homework: + docker compose up --build + +test_homework: + docker compose --profile homework1_test run --rm --build homework1_test \ No newline at end of file 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 # Цели и задачи л/р: diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..e6ca0f2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'java' + id 'application' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.0' +} + +application { + mainClass = 'org.example.app.Main' +} + +configurations { + jcstress +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + showStandardStreams = true + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4373eea --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + homework1: + build: . + container_name: homework_1_run + command: ./gradlew run --no-daemon + + homework1_test: + build: . + container_name: homework1_test + profiles: + - homework1_test + command: ./gradlew clean test \ No newline at end of file diff --git a/readme.MD b/readme.MD new file mode 100644 index 0000000..e974d43 --- /dev/null +++ b/readme.MD @@ -0,0 +1,32 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/qcWcnElX) +# Java concurrency + +# Цели и задачи л/р: +Задача об обедающих философах: + +Рассмотрим семь программистов, сидящих вокруг круглого стола для обеда. +У каждого программиста есть тарелка супа перед ним, а между каждой парой программистов находится ложка. +Однако, чтобы поесть суп, программисту необходимо взять две ложки - справа и слева (он очень голодный). +Когда программист поедает суп, ложки остаются занятыми и не могут быть использованы соседними программистами. +Программисты чередуют прием еды с обсуждением преподавателей. +Когда суп заканчивается, программист просит одного из двух официантов принести ему еще одну порцию (то есть тарелка супа ограничена). +Всего в ресторане есть 1_000_000 порций еды, после чего обед заканчивается. +Все программисты должны поесть +- одинаково, чтобы никому не было обидно + + + +Ваша задача - реализовать симуляцию обеда с использованием языка программирования Java и многопоточности. +Каждый программист должен быть представлен в виде потока, а ложки - в виде общих ресурсов, которые программисты могут захватывать и освобождать. +Также не забудьте про официантов и запасы еды. + +Дополнительное условие -- количество программистов, еды и официантов должно быть параметризируемое. + +[Это усложнение классической задачи, про которую можно почитать тут](https://en.wikipedia.org/wiki/Dining_philosophers_problem) + +Необходимо обеспечить корректное выполнение программы, чтобы избежать состояний взаимной блокировки и гарантировать, что каждый программист получит возможность поесть. + +# Обязательное условие: +* Использование системы сборки Gradle +* Код должен быть отлажен и протестирован + +# Дедлайн 08.10.2025 23:59 \ No newline at end of file diff --git a/src/main/config.properties b/src/main/config.properties new file mode 100644 index 0000000..8e38782 --- /dev/null +++ b/src/main/config.properties @@ -0,0 +1,3 @@ +iterations=10 +programmers_count=50 +food_count=1000 \ No newline at end of file diff --git a/src/main/java/org/example/app/Main.java b/src/main/java/org/example/app/Main.java new file mode 100644 index 0000000..973dda0 --- /dev/null +++ b/src/main/java/org/example/app/Main.java @@ -0,0 +1,30 @@ +package org.example.app; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +import org.example.dinner.*; + +public class Main { + public static void main(String[] args) throws InterruptedException, IOException { + Properties props = new Properties(); + props.load(new FileInputStream("src/main/config.properties")); + + int iterations = Integer.parseInt(props.getProperty("iterations", "10")); + int fixedProgrammers = Integer.parseInt(props.getProperty("programmers_count", "-1")); + int fixedFood = Integer.parseInt(props.getProperty("food_count", "-1")); + + for (int i = 0; i < iterations; i++) { + AtomicInteger foodCount = new AtomicInteger(fixedFood); + + System.out.println("--------------------------------------------------------------------"); + System.out.println("Programmers: " + fixedProgrammers); + System.out.println("Food: " + fixedFood); + + Dinner dinner = new Dinner(); + dinner.serve(fixedProgrammers, foodCount); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/example/dinner/Dinner.java b/src/main/java/org/example/dinner/Dinner.java new file mode 100644 index 0000000..4906463 --- /dev/null +++ b/src/main/java/org/example/dinner/Dinner.java @@ -0,0 +1,48 @@ +package org.example.dinner; + +import org.example.programmer.Programmer; +import org.example.fork.Fork; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class Dinner { + + // Dependencies + private final DinnerVerifier verifier = new DinnerVerifier(); + private final ResourceGenerator resourceGenerator = new ResourceGenerator(); + private final ArrayList programmers = new ArrayList<>(); + private final BlockingQueue forks = new LinkedBlockingQueue<>(); + private HashMap counts = new HashMap<>(); + + public void serve(int programmersCount, AtomicInteger foodCount) throws InterruptedException { + // Verifying the values to not run with garbage ones + verifier.execute(programmersCount, foodCount); + // Generating resources + resourceGenerator.generate(programmers, forks, programmersCount, foodCount); + // Starting dinner + ExecutorService pool = Executors.newFixedThreadPool(programmersCount); + for (Programmer p : programmers) { + pool.submit(p); + } + pool.shutdown(); + pool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + System.out.println("Dinner is over!"); + System.out.println("Food amount is " + foodCount); + // Counting stats to display + for (Programmer programmer : programmers) { + int count = counts.getOrDefault(programmer.getPortionsEaten(), 0); + counts.put(programmer.getPortionsEaten(), count + 1); + } + // Displaying stats + for (Map.Entry entry : counts.entrySet()) { + int portionsEaten = entry.getKey(); + int count = entry.getValue(); + System.out.println("Portions eaten: " + portionsEaten + ", Count: " + count); + } + System.out.println("--------------------------------------------------------------------"); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/dinner/DinnerVerifier.java b/src/main/java/org/example/dinner/DinnerVerifier.java new file mode 100644 index 0000000..bcc83ee --- /dev/null +++ b/src/main/java/org/example/dinner/DinnerVerifier.java @@ -0,0 +1,15 @@ +package org.example.dinner; + +import java.util.concurrent.atomic.AtomicInteger; + +public class DinnerVerifier { + public void execute(int ProgrammersCount, AtomicInteger FoodCount) throws IllegalArgumentException { + // Verifying that the amount can't be negative + if (ProgrammersCount <= 0 || FoodCount.get() <= 0) { + throw new IllegalArgumentException("Amount can't be negative"); + } + if (FoodCount.get() < ProgrammersCount) { + throw new IllegalArgumentException("Some of programmers would still be hungry"); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/example/dinner/ResourceGenerator.java b/src/main/java/org/example/dinner/ResourceGenerator.java new file mode 100644 index 0000000..0a3f9ab --- /dev/null +++ b/src/main/java/org/example/dinner/ResourceGenerator.java @@ -0,0 +1,29 @@ +package org.example.dinner; + +import org.example.programmer.Programmer; +import org.example.fork.Fork; + +import java.util.ArrayList; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; + +public class ResourceGenerator { + public void generate( + ArrayList programmers, + BlockingQueue forks, + int amount, + AtomicInteger foodAmount + ) { + Semaphore availableForks = new Semaphore(amount, true); + // Generating the N amount of forks + for (int i = 0; i < amount; i++) { + forks.add(new Fork(i)); + } + // The same amount of programmers + for (int i = 0; i < amount; i++) { + programmers.add(new Programmer(i, forks, foodAmount, availableForks)); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/example/fork/Fork.java b/src/main/java/org/example/fork/Fork.java new file mode 100644 index 0000000..e768056 --- /dev/null +++ b/src/main/java/org/example/fork/Fork.java @@ -0,0 +1,19 @@ +package org.example.fork; + +import java.util.concurrent.locks.ReentrantLock; + +// Just fork +public class Fork implements Comparable { + int id; + private final ReentrantLock lock = new ReentrantLock(true); + + public Fork(int id) { + this.id = id; + } + public int getId() { return id; } + + @Override + public int compareTo(Fork other) { + return Integer.compare(this.id, other.id); + } +} diff --git a/src/main/java/org/example/programmer/Programmer.java b/src/main/java/org/example/programmer/Programmer.java new file mode 100644 index 0000000..5dccd29 --- /dev/null +++ b/src/main/java/org/example/programmer/Programmer.java @@ -0,0 +1,102 @@ +package org.example.programmer; + +import org.example.fork.Fork; +import java.lang.*; +import java.util.HashMap; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; + +public class Programmer implements Runnable { + + // Resources of the thread + private final int id; + final HashMap userForks; + private int portionsEaten; + private final Semaphore availableForks; + + // Mutual resources shared between threads + private final BlockingQueue forks; + private final AtomicInteger foodLeft; + + public Programmer( + int id, + BlockingQueue forks, + AtomicInteger foodLeft, + Semaphore availableForks + ) { + // Resources of the thread + this.id = id; + this.userForks = new HashMap<>(); + this.portionsEaten = 0; + this.availableForks = availableForks; + + // Mutual resources shared between threads + this.forks = forks; + this.foodLeft = foodLeft; + } + + @Override + public void run() { + try { + // Trying to eat while food is here + while (takeOnePortionIfAvailable()) { + boolean ate = false; + // Trying while the programmer eat + while (!ate) { + // If grabbed forks - eat + if (grabForks()) { + try { + EatDinnder(); + ate = true; + } finally { + releaseForks(); + } + } + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private boolean takeOnePortionIfAvailable() { + int cur = foodLeft.get(); + // No food available - stop + if (cur <= 0) return false; + if (foodLeft.compareAndSet(cur, cur - 1)) return true; + return false; + } + + public int getProgId(){ return this.id; } + public int getPortionsEaten(){ return this.portionsEaten; } + + private boolean grabForks() { + try { + availableForks.acquire(2); + Fork first = forks.take(); + Fork second = forks.take(); + userForks.put(1, first); + userForks.put(2, second); + return true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + private void releaseForks() throws InterruptedException { + Fork first = userForks.remove(1); + Fork second = userForks.remove(2); + if (first != null) forks.put(first); + if (second != null) forks.put(second); + availableForks.release(2); + } + + private void EatDinnder() throws InterruptedException { + // Increment the eaten portions + this.portionsEaten++; + // We are eating + Thread.sleep(10); + } +} \ No newline at end of file diff --git a/src/test/java/org/example/dinner/DinnerTest.java b/src/test/java/org/example/dinner/DinnerTest.java new file mode 100644 index 0000000..ad65239 --- /dev/null +++ b/src/test/java/org/example/dinner/DinnerTest.java @@ -0,0 +1,43 @@ +package org.example.dinner; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class DinnerTest { + + private Dinner dinner; + + @BeforeEach + void setUp() { + dinner = new Dinner(); + } + + @Test + void serve_withEnoughFood_doesNotThrow() { + int programmersCount = 3; + AtomicInteger foodCount = new AtomicInteger(5); + + assertDoesNotThrow(() -> dinner.serve(programmersCount, foodCount)); + } + + @Test + void serve_withExactFood_doesNotThrow() { + int programmersCount = 2; + AtomicInteger foodCount = new AtomicInteger(2); + + assertDoesNotThrow(() -> dinner.serve(programmersCount, foodCount)); + } + + @Test + void serve_withZeroFood_doesNotThrow() { + int programmersCount = 1; + AtomicInteger foodCount = new AtomicInteger(0); + + assertThrows(IllegalArgumentException.class, () -> dinner.serve(programmersCount, foodCount)); + } +} diff --git a/src/test/java/org/example/dinner/DinnerVerifierTest.java b/src/test/java/org/example/dinner/DinnerVerifierTest.java new file mode 100644 index 0000000..7c745ba --- /dev/null +++ b/src/test/java/org/example/dinner/DinnerVerifierTest.java @@ -0,0 +1,43 @@ +package org.example.dinner; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +class DinnerVerifierTest { + + private DinnerVerifier verifier; + + @BeforeEach + void setUp() { + verifier = new DinnerVerifier(); + } + + @Test + void execute_withValidValues_doesNotThrow() { + assertDoesNotThrow(() -> verifier.execute(3, new AtomicInteger(5))); + assertDoesNotThrow(() -> verifier.execute(2, new AtomicInteger(2))); + } + + @Test + void execute_withNegativeProgrammers_throws() { + assertThrows(IllegalArgumentException.class, + () -> verifier.execute(-1, new AtomicInteger(5))); + } + + @Test + void execute_withNegativeFood_throws() { + assertThrows(IllegalArgumentException.class, + () -> verifier.execute(3, new AtomicInteger(-2))); + } + + @Test + void execute_withInsufficientFood_throws() { + assertThrows(IllegalArgumentException.class, + () -> verifier.execute(5, new AtomicInteger(-3))); + } +} \ No newline at end of file diff --git a/src/test/java/org/example/dinner/ResourceGeneratorTest.java b/src/test/java/org/example/dinner/ResourceGeneratorTest.java new file mode 100644 index 0000000..dcbf59c --- /dev/null +++ b/src/test/java/org/example/dinner/ResourceGeneratorTest.java @@ -0,0 +1,37 @@ +package org.example.dinner; + +import org.example.fork.Fork; +import org.example.programmer.Programmer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class ResourceGeneratorTest { + + private ResourceGenerator generator; + private BlockingQueue forks; + private ArrayList programmers; + private AtomicInteger foodAmount; + + @BeforeEach + public void setUp() { + generator = new ResourceGenerator(); + forks = new LinkedBlockingQueue<>(); + programmers = new ArrayList<>(); + foodAmount = new AtomicInteger(100); + } + + @Test + void test_generate_values_not_null(){ + generator.generate(programmers, forks, 10, foodAmount); + assertNotEquals(programmers.size(), 0); + assertNotEquals(forks.size(), 0); + } +} diff --git a/src/test/java/org/example/fork/ForkTest.java b/src/test/java/org/example/fork/ForkTest.java new file mode 100644 index 0000000..f21d006 --- /dev/null +++ b/src/test/java/org/example/fork/ForkTest.java @@ -0,0 +1,16 @@ +package org.example.fork; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ForkTest { + + @Test + void testConstructorAndGetter() { + Fork fork0 = new Fork(0); + Fork fork1 = new Fork(1); + + assertEquals(0, fork0.getId(), "Fork id should be 0"); + assertEquals(1, fork1.getId(), "Fork id should be 1"); + } +} \ No newline at end of file diff --git a/src/test/java/org/example/programmer/ProgrammerTest.java b/src/test/java/org/example/programmer/ProgrammerTest.java new file mode 100644 index 0000000..ee5eb9d --- /dev/null +++ b/src/test/java/org/example/programmer/ProgrammerTest.java @@ -0,0 +1,367 @@ +package org.example.programmer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.example.fork.Fork; + +import java.util.Arrays; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +public class ProgrammerTest { + + private Programmer programmer; + private BlockingQueue forks; + private AtomicInteger foodLeft; + private Semaphore semaphore; + + @BeforeEach + public void setUp() { + forks = new LinkedBlockingQueue<>(); + forks.add(new Fork(0)); + forks.add(new Fork(1)); + foodLeft = new AtomicInteger(2); + semaphore = new Semaphore(2); + programmer = new Programmer(1, forks, foodLeft, semaphore); + } + + @Test + public void testProgrammerEat() { + programmer.run(); + int portionsEaten = programmer.getPortionsEaten(); + assertEquals(2, portionsEaten); + } + + @Test + void testProgrammerEatMultiplePortions() { + Programmer multiMealProgrammer = new Programmer(0, forks, foodLeft, semaphore); + multiMealProgrammer.run(); + assertEquals(2, multiMealProgrammer.getPortionsEaten()); + } + + @Test + void testTwoProgrammersGrabForks() throws InterruptedException { + Programmer p1 = new Programmer(0, forks, foodLeft, semaphore); + Programmer p2 = new Programmer(1, forks, foodLeft, semaphore); + + Thread t1 = new Thread(p1); + Thread t2 = new Thread(p2); + t1.start(); + t2.start(); + t1.join(); + t2.join(); + + assertEquals(2, forks.size()); + assertEquals(1, p1.getPortionsEaten()); + assertEquals(1, p2.getPortionsEaten()); + } + + @Test + void testConcurrentProgrammers() { + Programmer p1 = new Programmer(0, forks, foodLeft, semaphore); + Programmer p2 = new Programmer(1, forks, foodLeft, semaphore); + Programmer p3 = new Programmer(2, forks, foodLeft, semaphore); + + p1.run(); + p2.run(); + p3.run(); + + assertEquals(forks.size(), 2); + } + + @Test + void testRepeatedEating() { + Programmer p = new Programmer(0, forks, foodLeft, semaphore); + p.run(); + + assertEquals(2, p.getPortionsEaten()); + assertEquals(forks.size(), 2); + } + + @Test + void testForksConsistencyAfterConcurrentEating() throws InterruptedException { + BlockingQueue sharedForks = new LinkedBlockingQueue<>(); + sharedForks.put(new Fork(0)); + sharedForks.put(new Fork(1)); + + Programmer p1 = new Programmer(0, sharedForks, foodLeft, semaphore); + Programmer p2 = new Programmer(1, sharedForks, foodLeft, semaphore); + + Thread t1 = new Thread(p1); + Thread t2 = new Thread(p2); + t1.start(); + t2.start(); + t1.join(); + t2.join(); + + assertEquals(2, sharedForks.size()); + assertEquals(1, p1.getPortionsEaten()); + assertEquals(1, p2.getPortionsEaten()); + } + + @Test + void testZeroMeals() throws InterruptedException { + foodLeft = new AtomicInteger(0); + Programmer p = new Programmer(0, forks, foodLeft, semaphore); + Thread t = new Thread(p); + t.start(); + t.join(); + assertEquals(0, p.getPortionsEaten()); + assertEquals(2, forks.size()); + } + + @Test + void testNegativeMeals() throws InterruptedException { + foodLeft = new AtomicInteger(-10); + Programmer p = new Programmer(0, forks, foodLeft, semaphore); + Thread t = new Thread(p); + t.start(); + t.join(); + assertEquals(0, p.getPortionsEaten()); + } + + @Test + void testMissingForks() throws InterruptedException { + forks = new LinkedBlockingQueue<>(); + Programmer p = new Programmer(5, forks, foodLeft, semaphore); + Thread t = new Thread(p); + t.start(); + t.join(500); + assertEquals(0, p.getPortionsEaten()); + assertEquals(0, forks.size()); + } + + @Test + void testMoreProgrammersThanForks() throws InterruptedException { + Programmer p1 = new Programmer(0, forks, foodLeft, semaphore); + Programmer p2 = new Programmer(1, forks, foodLeft, semaphore); + Programmer p3 = new Programmer(2, forks, foodLeft, semaphore); + + Thread t1 = new Thread(p1); + Thread t2 = new Thread(p2); + Thread t3 = new Thread(p3); + t1.start(); + t2.start(); + t3.start(); + t1.join(); + t2.join(); + t3.join(); + + assertEquals(2, forks.size()); + int total = p1.getPortionsEaten() + p2.getPortionsEaten() + p3.getPortionsEaten(); + assertTrue(total <= 3); + } + + @Test + void testManyProgrammers() { + int numProgrammers = 10; + Programmer[] programmers = new Programmer[numProgrammers]; + + for (int i = 0; i < numProgrammers; i++) { + programmers[i] = new Programmer(i, forks, foodLeft, semaphore); + programmers[i].run(); + } + assertEquals(2, forks.size()); + } + + @Test + void testNoDeadlock() throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(3); + + Programmer p1 = new Programmer(0, forks, foodLeft, semaphore); + Programmer p2 = new Programmer(1, forks, foodLeft, semaphore); + Programmer p3 = new Programmer(2, forks, foodLeft, semaphore); + + Future f1 = executor.submit(p1); + Future f2 = executor.submit(p2); + Future f3 = executor.submit(p3); + + f1.get(5, TimeUnit.SECONDS); + f2.get(5, TimeUnit.SECONDS); + f3.get(5, TimeUnit.SECONDS); + + executor.shutdownNow(); + assertEquals(2, forks.size()); + } + + @Test + void testForksReturned() { + Programmer p = new Programmer(0, forks, foodLeft, semaphore); + p.run(); + assertEquals(2, forks.size()); + } + + @Test + void testNoForkDoubleOwnership() { + Programmer p1 = new Programmer(0, forks, foodLeft, semaphore); + Programmer p2 = new Programmer(1, forks, foodLeft, semaphore); + + p1.run(); + p2.run(); + + assertTrue(p1.userForks.isEmpty()); + assertTrue(p2.userForks.isEmpty()); + assertEquals(2, forks.size()); + } + + @Test + void testGetProgId() { + Programmer p = new Programmer(42, forks, foodLeft, semaphore); + assertEquals(42, p.getProgId()); + } + + @Test + void testIncrementPortions() throws InterruptedException { + Programmer p = new Programmer(0, forks, foodLeft, semaphore); + p.run(); + assertEquals(2, p.getPortionsEaten()); + } + + @Test + void testMultipleProgrammers() throws InterruptedException { + BlockingQueue forks = new LinkedBlockingQueue<>(); + forks.add(new Fork(0)); + forks.add(new Fork(1)); + + AtomicInteger foodLeft = new AtomicInteger(2); + + Programmer p1 = new Programmer(0, forks, foodLeft, semaphore); + Programmer p2 = new Programmer(1, forks, foodLeft, semaphore); + + Thread t1 = new Thread(p1); + Thread t2 = new Thread(p2); + t1.start(); + t2.start(); + t1.join(); + t2.join(); + + assertEquals(1, p1.getPortionsEaten()); + assertEquals(1, p2.getPortionsEaten()); + + assertEquals(2, forks.size()); + } + @Test + void testForksMutualExclusion() throws InterruptedException { + BlockingQueue sharedForks = new LinkedBlockingQueue<>(); + sharedForks.add(new Fork(0)); + sharedForks.add(new Fork(1)); + AtomicInteger food = new AtomicInteger(2); + + Programmer p1 = new Programmer(0, sharedForks, food, semaphore); + Programmer p2 = new Programmer(1, sharedForks, food, semaphore); + + Thread t1 = new Thread(p1); + Thread t2 = new Thread(p2); + t1.start(); + t2.start(); + t1.join(); + t2.join(); + + assertEquals(2, p1.getPortionsEaten() + p2.getPortionsEaten()); + assertEquals(2, sharedForks.size()); + } + @Test + void testNoOverEating() throws InterruptedException { + AtomicInteger food = new AtomicInteger(1); + BlockingQueue forks = new LinkedBlockingQueue<>(); + forks.put(new Fork(0)); + forks.put(new Fork(1)); + + Programmer p1 = new Programmer(0, forks, food, semaphore); + Programmer p2 = new Programmer(1, forks, food, semaphore); + + p1.run(); + p2.run(); + + assertEquals(1, p1.getPortionsEaten() + p2.getPortionsEaten()); + } + + @Test + void testManyProgrammersLimitedFood() throws InterruptedException { + AtomicInteger food = new AtomicInteger(3); + BlockingQueue forks = new LinkedBlockingQueue<>(); + forks.put(new Fork(0)); + forks.put(new Fork(1)); + + Programmer[] programmers = new Programmer[5]; + for (int i = 0; i < 5; i++) { + programmers[i] = new Programmer(i, forks, food, semaphore); + programmers[i].run(); + } + int total = 0; + for (Programmer p : programmers) total += p.getPortionsEaten(); + assertEquals(3, total); + assertEquals(2, forks.size()); + } + @Test + void testNoDeadlockTwoProgrammers() throws InterruptedException { + AtomicInteger food = new AtomicInteger(2); + BlockingQueue forks = new LinkedBlockingQueue<>(); + forks.put(new Fork(0)); + forks.put(new Fork(1)); + + Programmer p1 = new Programmer(0, forks, food, semaphore); + Programmer p2 = new Programmer(1, forks, food, semaphore); + + ExecutorService executor = Executors.newFixedThreadPool(2); + executor.submit(p1); + executor.submit(p2); + executor.shutdown(); + executor.awaitTermination(5, TimeUnit.SECONDS); + + assertEquals(2, p1.getPortionsEaten() + p2.getPortionsEaten()); + } + + @Test + void testFoodDistributionManyThreads() throws InterruptedException { + AtomicInteger food = new AtomicInteger(5); + BlockingQueue forks = new LinkedBlockingQueue<>(); + forks.put(new Fork(0)); + forks.put(new Fork(1)); + + Programmer[] programmers = new Programmer[10]; + for (int i = 0; i < 10; i++) { + programmers[i] = new Programmer(i, forks, food, semaphore); + programmers[i].run(); + } + + int total = Arrays.stream(programmers).mapToInt(Programmer::getPortionsEaten).sum(); + assertEquals(5, total); + assertEquals(2, forks.size()); + } + + @Test + void testForksAlwaysReturned() throws InterruptedException { + AtomicInteger food = new AtomicInteger(3); + BlockingQueue forks = new LinkedBlockingQueue<>(); + forks.put(new Fork(0)); + forks.put(new Fork(1)); + + Programmer p1 = new Programmer(0, forks, food, semaphore); + Programmer p2 = new Programmer(1, forks, food, semaphore); + Programmer p3 = new Programmer(2, forks, food, semaphore); + + p1.run(); p2.run(); p3.run(); + + assertEquals(2, forks.size()); + } + + @Test + void testAtomicFoodDecrement() throws InterruptedException { + AtomicInteger food = new AtomicInteger(1); + BlockingQueue forks = new LinkedBlockingQueue<>(); + forks.put(new Fork(0)); + forks.put(new Fork(1)); + + Programmer p1 = new Programmer(0, forks, food, semaphore); + Programmer p2 = new Programmer(1, forks, food, semaphore); + + p1.run(); + p2.run(); + + int totalEaten = p1.getPortionsEaten() + p2.getPortionsEaten(); + assertEquals(1, totalEaten); + } +}