diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4944549 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..8fc3979 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + file://$PROJECT_DIR$/src/test/java/org/labs/FairnessTest.java + 29 + + + + + \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/org/labs/Main.java b/src/main/java/org/labs/Main.java index 9917247..ba14c36 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -1,7 +1,28 @@ package org.labs; public class Main { - public static void main(String[] args) { - System.out.println("Hello, World!"); + + public static void main(String[] args) throws InterruptedException { + startDinner(7, 2, 1000); + } + + public static void startDinner(int progsCount, int waitersCount, int portions) throws InterruptedException { + SpoonManager spoonManager = new SpoonManager(progsCount); + Programmer[] programmers = new Programmer[progsCount]; + WaiterService waiterService = new WaiterService(waitersCount, portions); + for (int i = 0; i < progsCount; i++) { + programmers[i] = new Programmer(i, spoonManager, waiterService); + programmers[i].start(); + } + + int total = 0; + for (int i = 0; i < progsCount; i++) { + programmers[i].join(); + total += programmers[i].getEaten(); + } + + waiterService.shutdown(); + System.out.println("TOTAL EATEN " + total); } -} \ No newline at end of file +} + diff --git a/src/main/java/org/labs/Programmer.java b/src/main/java/org/labs/Programmer.java new file mode 100644 index 0000000..9bb07f6 --- /dev/null +++ b/src/main/java/org/labs/Programmer.java @@ -0,0 +1,80 @@ +package org.labs; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class Programmer extends Thread { + + private final int id; + private final SpoonManager spoonManager; + private final WaiterService waiterService; + + private boolean hasPortion; + private int eaten = 0; + + public Programmer(int id, SpoonManager manager, WaiterService waiterService) { + this.id = id; + this.spoonManager = manager; + this.waiterService = waiterService; + this.hasPortion = false; + } + + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + code(); + + hasPortion = requestFoodWithTimeout(eaten); + if (!hasPortion) { + break; + } + + spoonManager.aquireSpoons(id); + try { + eat(); + } finally { + spoonManager.releaseSpoons(id); + } + } + System.out.println("Programmer-" + id + " has eaten " + eaten + " portions"); + } + + public int getEaten() { + return eaten; + } + + private boolean requestFoodWithTimeout(int eaten) { + try { + CompletableFuture getFoodFuture = waiterService.requestFood(eaten); + return getFoodFuture.get(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } catch (ExecutionException e) { + System.err.println("Waiter failed to bring new portion: " + e.getCause()); + return false; + } catch (TimeoutException e) { + System.err.println("No response from waiter"); + return false; + } + } + + private void eat() { + try { + Thread.sleep(TimeUtil.randomTime(TimeUtil.MIN_WAIT_TIME, TimeUtil.MAX_WAIT_TIME)); + eaten++; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void code() { + try { + Thread.sleep(TimeUtil.randomTime(TimeUtil.MIN_WAIT_TIME, TimeUtil.MAX_WAIT_TIME)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/java/org/labs/SpoonManager.java b/src/main/java/org/labs/SpoonManager.java new file mode 100644 index 0000000..d40aaae --- /dev/null +++ b/src/main/java/org/labs/SpoonManager.java @@ -0,0 +1,37 @@ +package org.labs; + +import java.util.concurrent.locks.ReentrantLock; + +public class SpoonManager { + private final int spoonCount; + private final ReentrantLock[] issued; + + public SpoonManager(int spoonCount) { + this.spoonCount = spoonCount; + this.issued = new ReentrantLock[spoonCount]; + for (int i = 0; i < spoonCount; i++) { + issued[i] = new ReentrantLock(); + } + } + + public void aquireSpoons(int programmerId) { + int leftSpoonIdx = leftSpoonIdx(programmerId); + int rightSpoonIdx = rightSpoonIdx(programmerId); + + issued[Math.min(leftSpoonIdx, rightSpoonIdx)].lock(); + issued[Math.max(leftSpoonIdx, rightSpoonIdx)].lock(); + } + + public void releaseSpoons(int programmerId) { + issued[leftSpoonIdx(programmerId)].unlock(); + issued[rightSpoonIdx(programmerId)].unlock(); + } + + private int rightSpoonIdx(int programmerId) { + return programmerId; + } + + private int leftSpoonIdx(int programmerId) { + return (programmerId + 1) % spoonCount; + } +} diff --git a/src/main/java/org/labs/TimeUtil.java b/src/main/java/org/labs/TimeUtil.java new file mode 100644 index 0000000..34b2f08 --- /dev/null +++ b/src/main/java/org/labs/TimeUtil.java @@ -0,0 +1,14 @@ +package org.labs; + +public class TimeUtil { + public static final Integer MIN_WAIT_TIME = 1; + public static final Integer MAX_WAIT_TIME = 3; + + public static long randomTime(int min, int max) { + return min + (long)(Math.random() * (max - min)); + } + + private TimeUtil() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/org/labs/Waiter.java b/src/main/java/org/labs/Waiter.java new file mode 100644 index 0000000..1a3bb25 --- /dev/null +++ b/src/main/java/org/labs/Waiter.java @@ -0,0 +1,35 @@ +package org.labs; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +public class Waiter extends Thread { + + private final int id; + private final BlockingQueue taskQueue; + + public Waiter(int id, BlockingQueue taskQueue) { + this.id = id; + this.taskQueue = taskQueue; + } + + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + WaiterTask task = taskQueue.poll(1, TimeUnit.SECONDS); + if (task != null) { + task.run(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + System.out.printf("Waiter-%d is shutting down%n", id); + } + + public void stopWorking() { + this.interrupt(); + } +} diff --git a/src/main/java/org/labs/WaiterService.java b/src/main/java/org/labs/WaiterService.java new file mode 100644 index 0000000..249ccf6 --- /dev/null +++ b/src/main/java/org/labs/WaiterService.java @@ -0,0 +1,51 @@ +package org.labs; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Сервис для обслуживания программистов + */ +public class WaiterService { + private final AtomicInteger portions; + private final PriorityBlockingQueue blockingQueue; + private final Waiter[] waiters; + + public WaiterService(int waitersCount, int portionCount) { + blockingQueue = new PriorityBlockingQueue<>(); + portions = new AtomicInteger(portionCount); + waiters = new Waiter[waitersCount]; + for (int i = 0; i < waitersCount; i++) { + waiters[i] = new Waiter(i, blockingQueue); + } + + startWaiters(); + } + + public CompletableFuture requestFood(int eatenBefore) throws InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + blockingQueue.offer(new WaiterTask(eatenBefore, portions, future)); + return future; + } + + private void startWaiters() { + for (Waiter waiter : waiters) { + waiter.start(); + } + } + + public void shutdown() { + for (Waiter waiter : waiters) { + waiter.stopWorking(); + } + + for (Waiter waiter : waiters) { + try { + waiter.join(3000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/src/main/java/org/labs/WaiterTask.java b/src/main/java/org/labs/WaiterTask.java new file mode 100644 index 0000000..59cb389 --- /dev/null +++ b/src/main/java/org/labs/WaiterTask.java @@ -0,0 +1,40 @@ +package org.labs; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; + +public class WaiterTask implements Runnable, Comparable { + private final int eatenBefore; + private final AtomicInteger portions; + private final CompletableFuture resultFuture; + + public WaiterTask(int eatenBefore, AtomicInteger portions, CompletableFuture resultFuture) { + this.eatenBefore = eatenBefore; + this.portions = portions; + this.resultFuture = resultFuture; + } + + @Override + public void run() { + try { + Thread.sleep(TimeUtil.randomTime(TimeUtil.MIN_WAIT_TIME, TimeUtil.MAX_WAIT_TIME)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + int remainingPortions = portions.get(); + if (remainingPortions % 5000 == 0) { + System.out.println("Portions left - " + remainingPortions); + } + boolean result = portions.getAndDecrement() > 0; + resultFuture.complete(result); + } + + @Override + public int compareTo(WaiterTask o) { + return Integer.compare(this.eatenBefore, o.eatenBefore); + } + + public int getEatenBefore() { + return eatenBefore; + } +} diff --git a/src/test/java/org/labs/FairnessTest.java b/src/test/java/org/labs/FairnessTest.java new file mode 100644 index 0000000..7beec40 --- /dev/null +++ b/src/test/java/org/labs/FairnessTest.java @@ -0,0 +1,37 @@ +package org.labs; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class FairnessTest { + + @Test + void testFairPortionDistribution() throws InterruptedException { + int progsCount = 7; + int waitersCount = 2; + int portions = 10000; + + SpoonManager spoonManager = new SpoonManager(progsCount); + Programmer[] programmers = new Programmer[progsCount]; + WaiterService waiterService = new WaiterService(waitersCount, portions); + for (int i = 0; i < progsCount; i++) { + programmers[i] = new Programmer(i, spoonManager, waiterService); + programmers[i].start(); + } + + for (int i = 0; i < progsCount; i++) { + programmers[i].join(20000); + } + + double possibleDeviation = 0.1; + double average = portions / (double) progsCount; + int totalEaten = 0; + for (Programmer programmer : programmers) { + assertTrue(Math.abs(average - programmer.getEaten()) / average <= possibleDeviation); + totalEaten += programmer.getEaten(); + } + waiterService.shutdown(); + + assertEquals(portions, totalEaten); + } +} diff --git a/src/test/java/org/labs/ProgrammerTest.java b/src/test/java/org/labs/ProgrammerTest.java new file mode 100644 index 0000000..900dbef --- /dev/null +++ b/src/test/java/org/labs/ProgrammerTest.java @@ -0,0 +1,36 @@ +package org.labs; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ProgrammerTest { + + @Test + void testEarlyInterruption() throws InterruptedException { + int progsCount = 7; + int waitersCount = 2; + int portions = 10000; + + SpoonManager spoonManager = new SpoonManager(progsCount); + Programmer[] programmers = new Programmer[progsCount]; + WaiterService waiterService = new WaiterService(waitersCount, portions); + for (int i = 0; i < progsCount; i++) { + programmers[i] = new Programmer(i, spoonManager, waiterService); + programmers[i].start(); + } + + Thread.sleep(1000); + + int totalEaten = 0; + for (Programmer programmer : programmers) { + programmer.interrupt(); + programmer.join(); + totalEaten += programmer.getEaten(); + } + + waiterService.shutdown(); + + assertTrue(totalEaten < portions); + } +} diff --git a/src/test/java/org/labs/WaiterTest.java b/src/test/java/org/labs/WaiterTest.java new file mode 100644 index 0000000..1e0ca5f --- /dev/null +++ b/src/test/java/org/labs/WaiterTest.java @@ -0,0 +1,31 @@ +package org.labs; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class WaiterTest { + + @Test + public void testWaiterPriorityQueue() throws InterruptedException { + PriorityBlockingQueue queue = new PriorityBlockingQueue<>(); + AtomicInteger portions = new AtomicInteger(100); + + queue.offer(createTask(10, portions)); + queue.offer(createTask(5, portions)); + queue.offer(createTask(15, portions)); + + assertEquals(5, queue.take().getEatenBefore()); + assertEquals(10, queue.take().getEatenBefore()); + assertEquals(15, queue.take().getEatenBefore()); + } + + + private WaiterTask createTask(int eatenBefore, AtomicInteger portions) { + return new WaiterTask(eatenBefore, portions, new CompletableFuture<>()); + } +} \ No newline at end of file