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 @@ +[![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.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, "Нельзя съесть больше порций чем есть"); + } +}