diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..ce1c62c
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
\ 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 @@
+[](https://classroom.github.com/a/qcWcnElX)
# Java concurrency
# Цели и задачи л/р:
diff --git a/build.gradle.kts b/build.gradle.kts
index bda0d97..974a504 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -10,10 +10,15 @@ repositories {
}
dependencies {
+ implementation("org.openjdk.jcstress:jcstress-core:0.16")
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
+ testImplementation ("org.junit.jupiter:junit-jupiter-params:5.9.3")
}
tasks.test {
useJUnitPlatform()
+ testLogging {
+ showStandardStreams = false
+ }
}
\ No newline at end of file
diff --git a/src/main/java/org/labs/Main.java b/src/main/java/org/labs/Main.java
index 9917247..7528906 100644
--- a/src/main/java/org/labs/Main.java
+++ b/src/main/java/org/labs/Main.java
@@ -1,7 +1,30 @@
package org.labs;
+import org.labs.config.AppConfig;
+import org.labs.config.ExecutorType;
+import org.labs.model.Restaurant;
+
public class Main {
+
public static void main(String[] args) {
- System.out.println("Hello, World!");
+ int numProgrammers = AppConfig.getNumProgrammers();
+ int numWaiters = AppConfig.getNumWaiters();
+ int maxMeals = AppConfig.getMaxMeals();
+ ExecutorType executorType = AppConfig.getExecutorType();
+ Restaurant restaurant = new Restaurant(maxMeals, numProgrammers, numWaiters, executorType);
+
+ long startTime = System.currentTimeMillis();
+ restaurant.startDinner();
+
+
+ restaurant.awaitCompletion();
+
+ long endTime = System.currentTimeMillis();
+ long duration = endTime - startTime;
+
+ restaurant.printStatistics();
+
+ System.out.println("\nTotal dinner time: " + duration + " ms");
+ System.out.println("Dinner completed successfully!");
}
}
\ No newline at end of file
diff --git a/src/main/java/org/labs/config/AppConfig.java b/src/main/java/org/labs/config/AppConfig.java
new file mode 100644
index 0000000..bd4433e
--- /dev/null
+++ b/src/main/java/org/labs/config/AppConfig.java
@@ -0,0 +1,52 @@
+package org.labs.config;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+public class AppConfig {
+ private static final Properties props = new Properties();
+
+ static {
+ try (InputStream input = AppConfig.class.getClassLoader()
+ .getResourceAsStream("config.properties")) {
+ if (input == null) {
+ System.err.println("Config file not found! Using defaults");
+ setDefaults();
+ } else {
+ props.load(input);
+ }
+ } catch (IOException e) {
+ System.err.println("Error loading config: " + e.getMessage());
+ setDefaults();
+ }
+ }
+
+ private static void setDefaults() {
+ props.setProperty("num.programmers", "7");
+ props.setProperty("num.waiters", "3");
+ props.setProperty("max.meals", "1_000_000");
+ props.setProperty("executor.type", "VIRTUAL_THREADS");
+ }
+
+ public static int getNumProgrammers() {
+ return Integer.parseInt(props.getProperty("num.programmers"));
+ }
+
+ public static int getNumWaiters() {
+ return Integer.parseInt(props.getProperty("num.waiters"));
+ }
+
+ public static int getMaxMeals() {
+ return Integer.parseInt(props.getProperty("max.meals"));
+ }
+
+ public static ExecutorType getExecutorType() {
+ try {
+ return ExecutorType.valueOf(props.getProperty("executor.type").toUpperCase());
+ } catch (IllegalArgumentException e) {
+ System.err.println("Invalid executor type in config, using VIRTUAL_THREADS");
+ return ExecutorType.VIRTUAL_THREADS;
+ }
+ }
+}
diff --git a/src/main/java/org/labs/config/ExecutorType.java b/src/main/java/org/labs/config/ExecutorType.java
new file mode 100644
index 0000000..d880460
--- /dev/null
+++ b/src/main/java/org/labs/config/ExecutorType.java
@@ -0,0 +1,8 @@
+package org.labs.config;
+
+public enum ExecutorType {
+ VIRTUAL_THREADS,
+ FIXED_THREAD_POOL,
+ CACHED_THREAD_POOL,
+ WORK_STEALING_POOL,
+}
diff --git a/src/main/java/org/labs/model/Kitchen.java b/src/main/java/org/labs/model/Kitchen.java
new file mode 100644
index 0000000..e4ed3c5
--- /dev/null
+++ b/src/main/java/org/labs/model/Kitchen.java
@@ -0,0 +1,28 @@
+package org.labs.model;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class Kitchen {
+ private final AtomicInteger remainingMeals;
+ private volatile boolean isOpen = true;
+
+ public Kitchen(int totalMeals) {
+ this.remainingMeals = new AtomicInteger(totalMeals);
+ }
+
+ public boolean tryTakeMeal() {
+ return remainingMeals.getAndUpdate(current -> current > 0 ? current - 1 : current) > 0;
+ }
+
+ public boolean hasMeals() {
+ return remainingMeals.get() > 0 && isOpen;
+ }
+
+ public int getRemainingMeals() {
+ return remainingMeals.get();
+ }
+
+ public void close() {
+ isOpen = false;
+ }
+}
diff --git a/src/main/java/org/labs/model/Programmer.java b/src/main/java/org/labs/model/Programmer.java
new file mode 100644
index 0000000..e5a004a
--- /dev/null
+++ b/src/main/java/org/labs/model/Programmer.java
@@ -0,0 +1,110 @@
+package org.labs.model;
+
+import java.util.Comparator;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+class Programmer implements Runnable {
+ private final int id;
+ private final Spoon firstSpoon;
+ private final Spoon secondSpoon;
+ private final Waiter waiter;
+ private final AtomicInteger mealsEaten = new AtomicInteger(0);
+ private final AtomicBoolean permissionToEat = new AtomicBoolean(false);
+ private volatile boolean running = true;
+
+ public Programmer(int id, Spoon leftSpoon, Spoon rightSpoon, Waiter waiter) {
+ this.id = id;
+ this.waiter = waiter;
+
+ // Определяем порядок взятия ложек для избежания deadlock
+ if (leftSpoon.getId() < rightSpoon.getId()) {
+ this.firstSpoon = leftSpoon;
+ this.secondSpoon = rightSpoon;
+ } else {
+ this.firstSpoon = rightSpoon;
+ this.secondSpoon = leftSpoon;
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (running && waiter.isKitchenOpen()) {
+ think();
+ requestToEat();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ System.out.println("Programmer " + id + " finished. Eaten: " + mealsEaten.get() + " meals");
+ }
+ }
+
+ private void think() throws InterruptedException {
+ System.out.println("Programmer " + id + " is thinking");
+ Thread.sleep(ThreadLocalRandom.current().nextInt(100, 200));
+ }
+
+ private void requestToEat() throws InterruptedException {
+ if (!running || !waiter.isKitchenOpen()) {
+ return;
+ }
+
+ permissionToEat.set(false);
+ waiter.placeOrder(this);
+
+ synchronized (this) {
+ if (!permissionToEat.get()) {
+ wait(5000); // Долгий таймаут, просто чтобы не блокировать навсегда
+ }
+ }
+
+ if (permissionToEat.get() && running) {
+ eat();
+ }
+ }
+
+ private void eat() throws InterruptedException {
+ System.out.println("Programmer " + id + " has food, waiting for spoons...");
+
+ firstSpoon.acquire();
+ try {
+ secondSpoon.acquire();
+
+ Thread.sleep(ThreadLocalRandom.current().nextInt(100, 200));
+ try {
+ System.out.println("Programmer " + id + " is eating (" + (mealsEaten.get() + 1) + " meal)");
+ mealsEaten.incrementAndGet();
+ System.out.println("Programmer " + id + " finished eating");
+ } finally {
+ secondSpoon.release();
+ }
+ } finally {
+ firstSpoon.release();
+ }
+ }
+
+ public void receiveMeal() {
+ permissionToEat.set(true);
+ synchronized (this) {
+ notify();
+ }
+ }
+
+ public void stop() {
+ running = false;
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+
+ public static Comparator getMealsEatenComparator() {
+ return Comparator.comparingInt(Programmer::getMealsEaten);
+ }
+
+ public int getMealsEaten() { return mealsEaten.get(); }
+ public void setPermissionToEat(boolean permission) { permissionToEat.set(permission); }
+ public int getId() { return id; }
+}
diff --git a/src/main/java/org/labs/model/Restaurant.java b/src/main/java/org/labs/model/Restaurant.java
new file mode 100644
index 0000000..31fe6b5
--- /dev/null
+++ b/src/main/java/org/labs/model/Restaurant.java
@@ -0,0 +1,154 @@
+package org.labs.model;
+
+import org.labs.config.ExecutorType;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+public class Restaurant {
+ private final Kitchen kitchen;
+ private final Spoon[] spoons;
+ private final Programmer[] programmers;
+ private final Waiter[] waiters;
+ private final ExecutorService executor;
+
+ public Restaurant(int totalMeals, int numProgrammers, int numWaiters, ExecutorType executorType) {
+ // Создаем кухню
+ this.kitchen = new Kitchen(totalMeals);
+
+ // Создаем ложки
+ this.spoons = new Spoon[numProgrammers];
+ for (int i = 0; i < numProgrammers; i++) {
+ spoons[i] = new Spoon(i);
+ }
+
+ // Создаем официантов
+ this.waiters = new Waiter[numWaiters];
+ for (int i = 0; i < numWaiters; i++) {
+ waiters[i] = new Waiter(i, kitchen);
+ }
+
+ // Создаем программистов и распределяем по официантам
+ this.programmers = new Programmer[numProgrammers];
+ for (int i = 0; i < numProgrammers; i++) {
+ Spoon leftSpoon = spoons[i];
+ Spoon rightSpoon = spoons[(i + 1) % numProgrammers];
+ Waiter assignedWaiter = waiters[i % numWaiters];
+ programmers[i] = new Programmer(i, leftSpoon, rightSpoon, assignedWaiter);
+ }
+
+ // Создаем executor по переданному типу
+ this.executor = createExecutor(executorType, numProgrammers, numWaiters);
+ }
+
+ // Ресторан работает так, что программисты перед поеданием супа просят официанта принести еду
+ // Официант работает как очередь заказов, а кухня предоставляет доступ к еде для официантов
+ public void startDinner() {
+ for (Waiter waiter : waiters) {
+ executor.execute(waiter);
+ }
+ for (Programmer programmer : programmers) {
+ executor.execute(programmer);
+ }
+ }
+
+ public void awaitCompletion() {
+ executor.shutdown();
+ try {
+ if (!executor.awaitTermination(5, TimeUnit.MINUTES)) {
+ System.err.println("Dinner timed out! Forcing shutdown...");
+ executor.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } finally {
+ kitchen.close();
+ }
+ }
+
+ public void stopDinner() {
+ kitchen.close();
+ for (Programmer programmer : programmers) {
+ programmer.stop();
+ }
+ for (Waiter waiter : waiters) {
+ waiter.stop();
+ }
+ executor.shutdownNow();
+ }
+
+ public void printStatistics() {
+ int totalEaten = 0;
+ int minEaten = Integer.MAX_VALUE;
+ int maxEaten = Integer.MIN_VALUE;
+
+ for (Programmer programmer : programmers) {
+ int eaten = programmer.getMealsEaten();
+ totalEaten += eaten;
+ minEaten = Math.min(minEaten, eaten);
+ maxEaten = Math.max(maxEaten, eaten);
+ }
+
+ double averageEaten = (double) totalEaten / programmers.length;
+
+ System.out.printf("Remaining meals: %d%n", kitchen.getRemainingMeals());
+ System.out.printf("Total meals eaten: %d%n", totalEaten);
+ System.out.printf("Programmers: %d%n", programmers.length);
+ System.out.printf("Average per programmer: %.2f meals%n", averageEaten);
+ System.out.printf("Range: %d - %d meals%n", minEaten, maxEaten);
+ System.out.printf("Difference: %d meals%n", maxEaten - minEaten);
+ }
+
+ public int getTotalMealsEaten() {
+ int totalEaten = 0;
+
+ for (Programmer programmer : programmers) {
+ int eaten = programmer.getMealsEaten();
+ totalEaten += eaten;
+ }
+ return totalEaten;
+ }
+
+ public int getEatenMealDifference() {
+ if (programmers == null || programmers.length == 0) {
+ return 0;
+ }
+
+ int minMeals = Integer.MAX_VALUE;
+ int maxMeals = Integer.MIN_VALUE;
+
+ for (Programmer programmer : programmers) {
+ int mealsEaten = programmer.getMealsEaten();
+ minMeals = Math.min(minMeals, mealsEaten);
+ maxMeals = Math.max(maxMeals, mealsEaten);
+ }
+
+ return maxMeals - minMeals;
+ }
+
+ private ExecutorService createExecutor(ExecutorType type, int numProgrammers, int numWaiters) {
+ switch (type) {
+ case VIRTUAL_THREADS:
+ System.out.println("Using Virtual Threads");
+ return Executors.newVirtualThreadPerTaskExecutor();
+
+ case FIXED_THREAD_POOL:
+ int poolSize = numProgrammers + numWaiters;
+ System.out.println("Using Fixed Thread Pool: " + poolSize + " threads");
+ return Executors.newFixedThreadPool(poolSize);
+
+ case CACHED_THREAD_POOL:
+ System.out.println("Using Cached Thread Pool");
+ return Executors.newCachedThreadPool();
+
+ case WORK_STEALING_POOL:
+ int parallelism = Math.max(1, Runtime.getRuntime().availableProcessors());
+ System.out.println("Using Work Stealing Pool: " + parallelism + " parallelism");
+ return Executors.newWorkStealingPool(parallelism);
+
+ default:
+ throw new IllegalArgumentException("Unknown executor type: " + type);
+ }
+ }
+}
diff --git a/src/main/java/org/labs/model/Spoon.java b/src/main/java/org/labs/model/Spoon.java
new file mode 100644
index 0000000..4ef7bf1
--- /dev/null
+++ b/src/main/java/org/labs/model/Spoon.java
@@ -0,0 +1,29 @@
+package org.labs.model;
+
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+public class Spoon {
+ private final Lock lock = new ReentrantLock(true);
+ private final int id;
+
+ public Spoon(int id) {
+ this.id = id;
+ }
+
+ public boolean tryAcquire() {
+ return lock.tryLock();
+ }
+
+ public void acquire() throws InterruptedException {
+ lock.lockInterruptibly();
+ }
+
+ public void release() {
+ lock.unlock();
+ }
+
+ public int getId() {
+ return id;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/labs/model/Waiter.java b/src/main/java/org/labs/model/Waiter.java
new file mode 100644
index 0000000..ffbb45b
--- /dev/null
+++ b/src/main/java/org/labs/model/Waiter.java
@@ -0,0 +1,69 @@
+package org.labs.model;
+
+import java.util.Collection;
+import java.util.PriorityQueue;
+import java.util.concurrent.PriorityBlockingQueue;
+
+public class Waiter implements Runnable {
+ private final int id;
+ private final Kitchen kitchen;
+ private final PriorityBlockingQueue programmerQueue;
+ private volatile boolean running = true;
+
+ public Waiter(int id, Kitchen kitchen) {
+ this.id = id;
+ this.kitchen = kitchen;
+ this.programmerQueue = new PriorityBlockingQueue<>(10, Programmer.getMealsEatenComparator());
+ }
+
+ public void placeOrder(Programmer programmer) {
+ if (running && kitchen.hasMeals()) {
+ programmerQueue.offer(programmer);
+ }
+ }
+
+ public boolean isKitchenOpen() {
+ return kitchen.hasMeals() && running;
+ }
+
+ @Override
+ public void run() {
+ System.out.println("Waiter " + id + " start working");
+
+ while (running && kitchen.hasMeals()) {
+ Programmer programmer = programmerQueue.poll();
+ if (programmer == null) {
+ continue;
+ }
+
+ // Пытаемся получить еду с кухни
+ if (kitchen.tryTakeMeal()) {
+ System.out.println("Waiter " + id + " serving meal to programmer " + programmer.getId());
+
+ // Сообщаем программисту, что еда готова
+ programmer.receiveMeal();
+ } else {
+ programmerQueue.offer(programmer);
+ }
+
+ }
+
+ notifyAllprogrammers();
+ System.out.println("Waiter " + id + " finished");
+ }
+
+ // после окончания работы очищаем очередь программистов
+ private void notifyAllprogrammers() {
+ Programmer programmer;
+ while ((programmer = programmerQueue.poll()) != null) {
+ programmer.setPermissionToEat(false);
+ synchronized (programmer) {
+ programmer.notify();
+ }
+ }
+ }
+
+ public void stop() {
+ running = false;
+ }
+}
diff --git a/src/main/resources/config.properties b/src/main/resources/config.properties
new file mode 100644
index 0000000..e691cc8
--- /dev/null
+++ b/src/main/resources/config.properties
@@ -0,0 +1,10 @@
+num.programmers=100
+num.waiters=3
+max.meals=100000
+
+
+executor.type=VIRTUAL_THREADS
+# executor.type=FIXED_THREAD_POOL
+# executor.type=CACHED_THREAD_POOL
+# executor.type=WORK_STEALING_POOL
+# executor.type=SINGLE_THREAD
\ No newline at end of file
diff --git a/src/test/java/ExecutorPerformanceTest.java b/src/test/java/ExecutorPerformanceTest.java
new file mode 100644
index 0000000..d271df3
--- /dev/null
+++ b/src/test/java/ExecutorPerformanceTest.java
@@ -0,0 +1,38 @@
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.labs.config.ExecutorType;
+import org.labs.model.Restaurant;
+
+import java.io.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ExecutorPerformanceTest {
+ @ParameterizedTest
+ @EnumSource(ExecutorType.class)
+ void testAllExecutorTypes(ExecutorType executorType) throws InterruptedException {
+ PrintStream originalOut = System.out;
+ try {
+ System.setOut(new PrintStream(new ByteArrayOutputStream()));
+
+ Restaurant restaurant = new Restaurant(1000, 50, 5, executorType);
+
+ long startTime = System.nanoTime();
+ restaurant.startDinner();
+ restaurant.awaitCompletion();
+ long endTime = System.nanoTime();
+
+ long durationMs = (endTime - startTime) / 1_000_000;
+ int mealsEaten = restaurant.getTotalMealsEaten();
+ int mealsDifference = restaurant.getEatenMealDifference();
+
+ System.setOut(originalOut);
+ System.out.printf("%s: %d ms, %d meals %d difference %n", executorType, durationMs, mealsEaten, mealsDifference);
+
+ assertTrue(mealsEaten > 0, "Should have eaten some meals");
+
+ } finally {
+ System.setOut(originalOut);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/MealLoadPerformanceTest.java b/src/test/java/MealLoadPerformanceTest.java
new file mode 100644
index 0000000..ba83e76
--- /dev/null
+++ b/src/test/java/MealLoadPerformanceTest.java
@@ -0,0 +1,44 @@
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.labs.config.ExecutorType;
+import org.labs.model.Restaurant;
+
+import java.io.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class MealLoadPerformanceTest {
+ @ParameterizedTest
+ @ValueSource(ints = {100, 1000, 10_000, 20_000})
+ void testWithMealAmount(int mealAmount) {
+ PrintStream originalOut = System.out;
+ try {
+ System.setOut(new PrintStream(new ByteArrayOutputStream()));
+
+ int numProgrammers = 100;
+ int numWaiters = 10;
+ ExecutorType executorType = ExecutorType.FIXED_THREAD_POOL;
+
+ Restaurant restaurant = new Restaurant(mealAmount, numProgrammers, numWaiters, executorType);
+
+ long startTime = System.nanoTime();
+ restaurant.startDinner();
+ restaurant.awaitCompletion();
+ long endTime = System.nanoTime();
+
+ long durationMs = (endTime - startTime) / 1_000_000;
+ int mealsEaten = restaurant.getTotalMealsEaten();
+ int mealsDifference = restaurant.getEatenMealDifference();
+ double mealsPerSecond = (double) mealsEaten / (durationMs / 1000.0);
+
+ System.setOut(originalOut);
+ System.out.printf("%s with %d meals: %d difference %d ms (%.2f meals/sec)%n",
+ executorType, mealAmount, mealsDifference, durationMs, mealsPerSecond);
+
+ assertEquals(mealAmount, mealsEaten, "All meals should be eaten");
+
+ } finally {
+ System.setOut(originalOut);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/RestaurantTest.java b/src/test/java/RestaurantTest.java
new file mode 100644
index 0000000..a30290e
--- /dev/null
+++ b/src/test/java/RestaurantTest.java
@@ -0,0 +1,30 @@
+
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.labs.config.ExecutorType;
+import org.labs.model.Restaurant;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class RestaurantTest {
+
+ private Restaurant restaurant;
+
+ @BeforeEach
+ void setUp() {
+ restaurant = new Restaurant(100, 5, 2, ExecutorType.VIRTUAL_THREADS);
+ }
+
+ @Test
+ @DisplayName("Все порции должны быть съедены")
+ void testAllMealsAreEaten() {
+ restaurant.startDinner();
+ restaurant.awaitCompletion();
+
+ int totalEaten = restaurant.getTotalMealsEaten();
+ assertTrue(totalEaten > 0, "Должны быть съедены порции");
+ assertTrue(totalEaten <= 100, "Нельзя съесть больше порций чем есть");
+ }
+}