diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..a0a9a28 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +parallel-lab11 \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..48b5565 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..61a27d7 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ 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/src/main/java/org/labs/Main.java b/src/main/java/org/labs/Main.java index 9917247..82da43f 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -1,7 +1,98 @@ package org.labs; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.LongAdder; +import java.util.Arrays; + public class Main { - public static void main(String[] args) { - System.out.println("Hello, World!"); + /** + * Точка входа в программу. Здесь задаются параметры выполняемыой программы (см. TaskStatic), вызывается выполнение программы, + * собираются результаты и выводятся в консоль + * @param args Не используется, так как статика задаётся хард-кодом внутри самого метода + * @throws InterruptedException Просто висит + */ + public static void main(String[] args) throws InterruptedException { + TaskStatic ts = new TaskStatic( // + 1, // programmersCount + 1, // waitersCount + 20, // rationsCount + 1, // eatTimeMillisMin + 3, // eatTimeMillisMax + 1, // talkTimeMillisMin + 3 // talkTimeMillisMax + ); + + Result r = startDinner(ts); + + Map rationEatenStats = new LinkedHashMap<>(); + for (int i = 0; i < r.eatenByProgrammer().length; i++) { + rationEatenStats.put(i, r.eatenByProgrammer()[i]); + } + + System.out.println("Rations eaten by each programmer - " + rationEatenStats); + System.out.println("Rations eaten totally - " + r.totalEaten()); + System.out.println("Rations left - " + r.rationsLeft()); } + + /** + * Основной выполняющий метод, вызывается из мейна. Здесь задаются необходимые объекты, по типу ложек и шкафа с едой + * и происходит само выполнение программы, с досугом рационного возъедания и болтовнёй + * @param ts Настройки программы (статика) + * @return Результат обеда + * @throws InterruptedException Зачем-то пробрасывается + */ + public static Result startDinner(TaskStatic ts) throws InterruptedException { + RationShelf rs = new RationShelf(ts.getRationsCount()); + Waiters w = new Waiters(ts.getWaitersCount(), ts.getProgrammersCount()); + + int programmersCount = ts.getProgrammersCount(); + + LongAdder[] la = new LongAdder[programmersCount]; + Arrays.setAll(la, _ -> new LongAdder()); + + Spoon[] s = new Spoon[programmersCount]; + for (int i = 0; i < programmersCount; i++) { + s[i] = new Spoon(i); + } + + try (ExecutorService programmersExec = Executors.newVirtualThreadPerTaskExecutor()) { + List programmersList = new ArrayList<>(programmersCount); + + try { + for (int i = 0; i < programmersCount; i++) { + Spoon l = s[i]; + Spoon r = s[(i + 1) % programmersCount]; + Programmer p = new Programmer(l, r, rs, w, ts, la[i]); + programmersList.add(p); + programmersExec.submit(p); + } + + while (rs.getLeftRations().get() > 0) { + Thread.sleep(TaskStatic.ProjectConfig.WAIT_UNTIL_RATION_SHELF_IS_EMPTY); + } + } finally { + for (Programmer p : programmersList) { + p.stop(); + } + programmersExec.shutdownNow(); + } + } + + long[] rationsEaten = new long[programmersCount]; + long total = 0; + for (int i = 0; i < programmersCount; i++) { + rationsEaten[i] = la[i].sum(); + total += rationsEaten[i]; + } + long left = rs.getLeftRations().get(); + + return new Result(rationsEaten, total, left); + } + } \ 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..9d78d7f --- /dev/null +++ b/src/main/java/org/labs/Programmer.java @@ -0,0 +1,179 @@ +package org.labs; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.LongAdder; + +/** + * Класс программистов, которые будут конкурентно есть еду + */ +public class Programmer implements Runnable { + private final Spoon leftSpoon; + private final Spoon rightSpoon; + private final RationShelf rs; + private final Waiters w; + private final TaskStatic ts; + private final LongAdder stats; + private volatile boolean running = true; + + public Programmer( + Spoon leftSpoon, + Spoon rightSpoon, + RationShelf rationShelf, + Waiters waiters, + TaskStatic taskStatic, + LongAdder stats + ) { + this.leftSpoon = leftSpoon; + this.rightSpoon = rightSpoon; + this.rs = rationShelf; + this.w = waiters; + this.ts = taskStatic; + this.stats = stats; + } + + /** + * Метод поедания еды программистом, здесь происходит периодическая болтовня, взятие ложек и поедание рационов + */ + @Override + public void run() { + while (running) { + try { + talkAboutTeachers(); + w.takeWaiter(stats); + try { + TwoSpoons sp = checkSpoons(); + if (tryTakeSpoons(sp)) { + try { + if (!rs.takeRation()) { + running = false; + continue; + } + eatRation(); + stats.increment(); + } finally { + returnSpoons(sp); + } + } + } finally { + w.letWaiterGo(); + } + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } catch (Throwable t) { + System.out.println("[programmer.run] throwable error - " + t.getMessage()); + } + } + } + + /** + * Метод для взятия ложек, проверяем можем ли мы взять обе ложки в каком-то временном интервале. + * Если нет, то возвращаем обе ложки + * @param sp Набор ложек, которые мы можем взять + * @return исход взятия обеих ложек (true если взяли обе + * @throws InterruptedException + */ + private boolean tryTakeSpoons(TwoSpoons sp) throws InterruptedException { + long waitTime = TaskStatic.ProjectConfig.PROGRAMMER_WAIT_TIME; + if (!sp.first.useWithTimeOut(waitTime)) { + return false; + } + boolean gotSecond; + try { + gotSecond = sp.second.useWithTimeOut(waitTime); + if (!gotSecond) { + sp.first.put(); + return false; + } + return true; + } catch (InterruptedException ie) { + sp.first.put(); + throw ie; + } + } + + /** + * Метод для болтовни, программист будет болтать какое-то случайное время, в рамках заданных настроек + * @throws InterruptedException + */ + private void talkAboutTeachers() throws InterruptedException { + action(ts.getTalkTimeMillisMin(), ts.getTalkTimeMillisMax()); + } + + /** + * То же самое, что и talkAboutTeachers, только для поедания еды (работает с другим временем) + * @throws InterruptedException + */ + private void eatRation() throws InterruptedException { + action(ts.getEatTimeMillisMin(), ts.getEatTimeMillisMax()); + } + + /** + * Метод проверки, что программист будет брать две ложки, которые лежат рядом с ним + * @return Объект-хелпер с двумя ложками, которые может взять программист + */ + private TwoSpoons checkSpoons() { + if (leftSpoon.getSpoonIndex() < rightSpoon.getSpoonIndex()) { + return new TwoSpoons(leftSpoon, rightSpoon); + } else { + return new TwoSpoons(rightSpoon, leftSpoon); + } + } + + /** + * Метод выполнения каког-то действия - есть или болтать + * @param minMs Минимальное время выполнения действия + * @param maxMs Максимальное время выполнения действия + * @throws InterruptedException + */ + private void action(long minMs, long maxMs) throws InterruptedException { + long span = Math.max(0, maxMs - minMs); + + long ms; + if (span == 0) { + ms = minMs; + } else { + ms = minMs + ThreadLocalRandom.current().nextLong(span); + } + + if (ms > 0) { + Thread.sleep(ms); + } + } + + /** + * Возможность позвать свободного оффицианта и взять у него рацион + * @return Исход возможности взятия рациона + * @throws InterruptedException + */ + private boolean takePortionWithWaiter() throws InterruptedException { + w.takeWaiter(stats); + try { + if (!rs.takeRation()) { + return false; + } + eatRation(); + stats.increment(); + return true; + } finally { + w.letWaiterGo(); + } + } + + /** + * Возврат обеих ложек + * @param sp Набор ложек конкретного программиста + */ + private void returnSpoons(TwoSpoons sp) { + sp.second.put(); + sp.first.put(); + } + + /** + * Стоп выполнения досуга поедания рациона + */ + public void stop() { + running = false; + } +} + diff --git a/src/main/java/org/labs/QueueNode.java b/src/main/java/org/labs/QueueNode.java new file mode 100644 index 0000000..28921ae --- /dev/null +++ b/src/main/java/org/labs/QueueNode.java @@ -0,0 +1,16 @@ +package org.labs; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicLong; + +public final class QueueNode { + final long hunger; + final long placeInQ; + final CountDownLatch latch = new CountDownLatch(1); + volatile boolean isReleased; + + QueueNode(long eaten, AtomicLong qOrder) { + this.hunger = eaten; + this.placeInQ = qOrder.getAndIncrement(); + } +} diff --git a/src/main/java/org/labs/RationShelf.java b/src/main/java/org/labs/RationShelf.java new file mode 100644 index 0000000..d71ebbd --- /dev/null +++ b/src/main/java/org/labs/RationShelf.java @@ -0,0 +1,38 @@ +package org.labs; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Шкаф со всеми рационами + */ +public class RationShelf { + private final AtomicLong rationLeft; + + public RationShelf(long rationLeft) { + this.rationLeft = new AtomicLong(rationLeft); + } + + /** + * Метод для взятия рациона, если ещё есть остатки + * @return Исход попытки взятия рациона + */ + public boolean takeRation() { + while (true) { + long curCount = rationLeft.get(); + if (curCount <= 0) { + return false; + } + if (rationLeft.compareAndSet(curCount, curCount - 1)) { + return true; + } + } + } + + /** + * Получить число оставшихся рационов + * @return Число оставшихся рационов + */ + public AtomicLong getLeftRations() { + return rationLeft; + } +} diff --git a/src/main/java/org/labs/Result.java b/src/main/java/org/labs/Result.java new file mode 100644 index 0000000..f1ecf11 --- /dev/null +++ b/src/main/java/org/labs/Result.java @@ -0,0 +1,22 @@ +package org.labs; + +public record Result( + long[] eatenByProgrammer, + long totalEaten, + long rationsLeft +) { + @Override + public long[] eatenByProgrammer() { + return eatenByProgrammer; + } + + @Override + public long totalEaten() { + return totalEaten; + } + + @Override + public long rationsLeft() { + return rationsLeft; + } +} diff --git a/src/main/java/org/labs/Spoon.java b/src/main/java/org/labs/Spoon.java new file mode 100644 index 0000000..80369a7 --- /dev/null +++ b/src/main/java/org/labs/Spoon.java @@ -0,0 +1,28 @@ +package org.labs; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +public class Spoon { + private final int spoonIndex; + private final ReentrantLock lock; + + public Spoon(int spoonIndex) { + this.spoonIndex = spoonIndex; + this.lock = new ReentrantLock(true); + } + + public int getSpoonIndex() { + return spoonIndex; + } + + public boolean useWithTimeOut(long timeoutMillis) throws InterruptedException { + return lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS); + } + + public void put() { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } +} diff --git a/src/main/java/org/labs/TaskStatic.java b/src/main/java/org/labs/TaskStatic.java new file mode 100644 index 0000000..c01e534 --- /dev/null +++ b/src/main/java/org/labs/TaskStatic.java @@ -0,0 +1,62 @@ +package org.labs; + +public class TaskStatic { + private final int programmersCount; + private final int waitersCount; + private final int rationsCount; + private final long eatTimeMillisMin; + private final long eatTimeMillisMax; + private final long talkTimeMillisMin; + private final long talkTimeMillisMax; + + public TaskStatic( + int programmersCount, + int waitersCount, + int rationsCount, + long eatTimeMillisMin, + long eatTimeMillisMax, + long talkTimeMillisMin, + long talkTimeMillisMax + ) { + this.programmersCount = programmersCount; + this.waitersCount = waitersCount; + this.rationsCount = rationsCount; + this.eatTimeMillisMin = eatTimeMillisMin; + this.eatTimeMillisMax = eatTimeMillisMax; + this.talkTimeMillisMin = talkTimeMillisMin; + this.talkTimeMillisMax = talkTimeMillisMax; + } + + public int getProgrammersCount() { + return programmersCount; + } + + public int getWaitersCount() { + return waitersCount; + } + + public int getRationsCount() { + return rationsCount; + } + + public long getEatTimeMillisMin() { + return eatTimeMillisMin; + } + + public long getEatTimeMillisMax() { + return eatTimeMillisMax; + } + + public long getTalkTimeMillisMin() { + return talkTimeMillisMin; + } + + public long getTalkTimeMillisMax() { + return talkTimeMillisMax; + } + + public static class ProjectConfig { + public static final int PROGRAMMER_WAIT_TIME = 3; + public static final int WAIT_UNTIL_RATION_SHELF_IS_EMPTY = 50; + } +} diff --git a/src/main/java/org/labs/TwoSpoons.java b/src/main/java/org/labs/TwoSpoons.java new file mode 100644 index 0000000..73344dd --- /dev/null +++ b/src/main/java/org/labs/TwoSpoons.java @@ -0,0 +1,14 @@ +package org.labs; + +/** + * Небольшой хелпер, где лежат ложки, которые может взять или уже взял какой-то программист + */ +public class TwoSpoons { + final Spoon first; + final Spoon second; + + TwoSpoons(Spoon first, Spoon second) { + this.first = first; + this.second = second; + } +} \ No newline at end of file diff --git a/src/main/java/org/labs/Waiters.java b/src/main/java/org/labs/Waiters.java new file mode 100644 index 0000000..575d617 --- /dev/null +++ b/src/main/java/org/labs/Waiters.java @@ -0,0 +1,78 @@ +package org.labs; + +import java.util.Comparator; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.atomic.LongAdder; + +/** + * Пулл официантов с приоритетной очередью, обслуживаем сначала самого голодного типочка + */ +public class Waiters { + private final AtomicInteger permits; + private final PriorityBlockingQueue queue; + private final ReentrantLock lock = new ReentrantLock(); + private static final AtomicLong qOrder = new AtomicLong(); + + public Waiters(int waitersCount, int programmersCount) { + this.permits = new AtomicInteger(waitersCount); + this.queue = new PriorityBlockingQueue<>(programmersCount, + Comparator.comparingLong((QueueNode a) -> a.hunger).thenComparingLong(a -> a.placeInQ)); + } + + /** + * Занять оффицианта с приоритетом по "голоду". + * @param stats счётчик съеденных рационов у программиста (чем голоднее, тем выше приоритет) + * @throws InterruptedException + */ + public void takeWaiter(LongAdder stats) throws InterruptedException { + QueueNode QueueNode = new QueueNode(stats.sum(), qOrder); + + lock.lock(); + try { + queue.add(QueueNode); + if (queue.peek() == QueueNode && permits.get() > 0) { + permits.decrementAndGet(); + queue.poll(); + QueueNode.isReleased = true; + QueueNode.latch.countDown(); + } + } finally { + lock.unlock(); + } + + try { + QueueNode.latch.await(); + } catch (InterruptedException ie) { + lock.lock(); + try { + if (!QueueNode.isReleased) { + queue.remove(QueueNode); + throw ie; + } + } finally { + lock.unlock(); + } + } + } + + /** + * Освободить оффицианта + */ + public void letWaiterGo() { + lock.lock(); + try { + QueueNode next = queue.poll(); + if (next != null) { + next.isReleased = true; + next.latch.countDown(); + } else { + permits.incrementAndGet(); + } + } finally { + lock.unlock(); + } + } +} diff --git a/src/test/java/org/labs/DinnerSimulationTest.java b/src/test/java/org/labs/DinnerSimulationTest.java new file mode 100644 index 0000000..9f38789 --- /dev/null +++ b/src/test/java/org/labs/DinnerSimulationTest.java @@ -0,0 +1,78 @@ +package org.labs; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Duration; +import java.util.*; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Tag("dinner") +class DinnerSimulationTest { + + static Stream testCases() { + return Stream.of( + new TestCaseSettings("1-1-20", 1, 1, 20, 1, 3, 1, 3), + new TestCaseSettings("5-2-200", 5, 2, 200, 1, 3, 1, 3), + new TestCaseSettings("7-2-300", 7, 2, 300, 2, 5, 2, 5), + new TestCaseSettings("8-5-300", 8, 5, 300, 1, 2, 1, 2), + new TestCaseSettings("10-1-400", 10, 1, 400, 1, 2, 1, 2), + new TestCaseSettings("30-1-3000", 30, 1, 1000, 1, 2, 1, 2), + new TestCaseSettings("7-2-10000", 7, 2, 10000, 1, 2, 1, 2) + ); + } + + @Order(1) + @DisplayName("Test ends in time without deadlocks") + @ParameterizedTest(name = "{index} => {0}") + @MethodSource("testCases") + void finishesWithinTimeout(TestCaseSettings sc) { + assertTimeoutPreemptively(Duration.ofSeconds(10), () -> { + Result r = Main.startDinner(sc.toTaskStatic()); + assertNotNull(r); + }); + } + + @Order(2) + @DisplayName("Ration counts correctly, no over/underusage") + @ParameterizedTest(name = "{index} => {0}") + @MethodSource("testCases") + void allRationsEaten(TestCaseSettings sc) throws InterruptedException { + Result r = Main.startDinner(sc.toTaskStatic()); + long sumEaten = Arrays.stream(r.eatenByProgrammer()).sum(); + long left = r.rationsLeft(); + + assertEquals(0L, left, "Should be zero rations left"); + assertEquals(sc.rations, sumEaten, "Eaten sum must equal total rations"); + } + + @Order(3) + @DisplayName("Everyone eats almost the same amount of rations (contention-aware)") + @ParameterizedTest(name = "{index} => {0}") + @MethodSource("testCases") + void fairOrNot(TestCaseSettings sc) throws InterruptedException { + Result r = Main.startDinner(sc.toTaskStatic()); + long[] arr = r.eatenByProgrammer().clone(); + Arrays.sort(arr); + long min = arr[0]; + long max = arr[arr.length - 1]; + + double allowedDelta = Math.sqrt(sc.rations / (double) sc.programmers); + + assertTrue((max - min) <= allowedDelta, + String.format("Wide dispersion: min=%d, max=%d, mean≈%.2f, allowed≤%.2f", + min, max, sc.rations / (double) sc.programmers, allowedDelta)); + +// assertTrue(false, +// String.format("Wide dispersion: min=%d, max=%d, mean≈%.2f, allowed≤%.2f", +// min, max, sc.rations / (double) sc.programmers, allowedDelta)); + } +} diff --git a/src/test/java/org/labs/TestCaseSettings.java b/src/test/java/org/labs/TestCaseSettings.java new file mode 100644 index 0000000..99f5c31 --- /dev/null +++ b/src/test/java/org/labs/TestCaseSettings.java @@ -0,0 +1,39 @@ +package org.labs; + +public final class TestCaseSettings { + final String name; + final int programmers; + final int waiters; + final int rations; + final int eatMin, eatMax; + final int talkMin, talkMax; + + TestCaseSettings( + String name, + int programmers, + int waiters, + int rations, + int eatMin, + int eatMax, + int talkMin, + int talkMax + ) { + this.name = name; + this.programmers = programmers; + this.waiters = waiters; + this.rations = rations; + this.eatMin = eatMin; + this.eatMax = eatMax; + this.talkMin = talkMin; + this.talkMax = talkMax; + } + + public TaskStatic toTaskStatic() { + return new TaskStatic(programmers, waiters, rations, eatMin, eatMax, talkMin, talkMax); + } + + @Override + public String toString() { + return String.format("%s[p=%d,w=%d,r=%d]", name, programmers, waiters, rations); + } +} \ No newline at end of file