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..da10ffa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,13 @@ repositories { dependencies { testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") + + implementation("org.slf4j:slf4j-api:2.0.9") + implementation("ch.qos.logback:logback-classic:1.4.11") + + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2") + implementation("com.fasterxml.jackson.core:jackson-core:2.15.2") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.15.2") } tasks.test { 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..b8a4ba9 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -1,7 +1,32 @@ package org.labs; +import org.labs.configuration.ConfigLoader; +import org.labs.configuration.SimulationConfig; +import org.labs.simulation.Simulation; +import org.labs.statistics.SimulationStatistics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.labs.simulation.SimulationUtils.createSimulation; +import static org.labs.simulation.SimulationUtils.runSimulation; + public class Main { + private static final Logger logger = LoggerFactory.getLogger(Main.class); + public static void main(String[] args) { - System.out.println("Hello, World!"); + logger.info("Starting Dining Programmers Problem Simulation"); + + try { + SimulationConfig config = ConfigLoader.loadDefaultConfig(); + logger.info("Loaded configuration: {}", config); + + Simulation simulation = createSimulation(config); + + SimulationStatistics statistics = runSimulation(simulation, config); + statistics.printFinalStatistics(simulation.plates()); + } catch (Exception e) { + logger.error("Simulation failed", e); + System.exit(1); + } } } \ No newline at end of file diff --git a/src/main/java/org/labs/configuration/ConfigLoader.java b/src/main/java/org/labs/configuration/ConfigLoader.java new file mode 100644 index 0000000..54077e1 --- /dev/null +++ b/src/main/java/org/labs/configuration/ConfigLoader.java @@ -0,0 +1,33 @@ +package org.labs.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; + +public class ConfigLoader { + private static final Logger logger = LoggerFactory.getLogger(ConfigLoader.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static SimulationConfig loadConfig(String configPath) { + try (InputStream inputStream = ConfigLoader.class.getClassLoader().getResourceAsStream(configPath)) { + if (inputStream == null) { + throw new RuntimeException("Configuration file not found: " + configPath); + } + + SimulationConfig config = objectMapper.readValue(inputStream, SimulationConfig.class); + logger.info("Loaded simulation configuration: {}", config); + return config; + + } catch (IOException e) { + logger.error("Failed to load configuration from {}", configPath, e); + throw new RuntimeException("Failed to load configuration", e); + } + } + + public static SimulationConfig loadDefaultConfig() { + return loadConfig("simulation-config.json"); + } +} diff --git a/src/main/java/org/labs/configuration/ProgrammerProperties.java b/src/main/java/org/labs/configuration/ProgrammerProperties.java new file mode 100644 index 0000000..109f4e9 --- /dev/null +++ b/src/main/java/org/labs/configuration/ProgrammerProperties.java @@ -0,0 +1,18 @@ +package org.labs.configuration; + +import java.util.concurrent.ThreadLocalRandom; + +public record ProgrammerProperties( + int minEatingTime, + int maxEatingTime, + int minChitChatTime, + int maxChitChatTime +) { + public long getEatingTime() { + return ThreadLocalRandom.current().nextLong(minEatingTime, maxEatingTime); + } + + public long getChitChatTime() { + return ThreadLocalRandom.current().nextLong(minChitChatTime, maxChitChatTime); + } +} diff --git a/src/main/java/org/labs/configuration/SimulationConfig.java b/src/main/java/org/labs/configuration/SimulationConfig.java new file mode 100644 index 0000000..034d8b7 --- /dev/null +++ b/src/main/java/org/labs/configuration/SimulationConfig.java @@ -0,0 +1,10 @@ +package org.labs.configuration; + +public record SimulationConfig( + int numberOfProgrammers, + int numberOfWaiters, + int totalFoodPortions, + ProgrammerProperties programmerProperties, + WaiterProperties waiterProperties +) { +} diff --git a/src/main/java/org/labs/configuration/WaiterProperties.java b/src/main/java/org/labs/configuration/WaiterProperties.java new file mode 100644 index 0000000..7395fdd --- /dev/null +++ b/src/main/java/org/labs/configuration/WaiterProperties.java @@ -0,0 +1,12 @@ +package org.labs.configuration; + +import java.util.concurrent.ThreadLocalRandom; + +public record WaiterProperties( + int minServingTime, + int maxServingTime +) { + public long getServingTime() { + return ThreadLocalRandom.current().nextLong(minServingTime, maxServingTime); + } +} diff --git a/src/main/java/org/labs/model/Plate.java b/src/main/java/org/labs/model/Plate.java new file mode 100644 index 0000000..cada89d --- /dev/null +++ b/src/main/java/org/labs/model/Plate.java @@ -0,0 +1,46 @@ +package org.labs.model; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +public class Plate implements Comparable { + private final AtomicReference plateState; + private final AtomicInteger numOfRefills; + private final int programmerId; + + public Plate(int programmerId) { + this.programmerId = programmerId; + this.plateState = new AtomicReference<>(PlateState.EMPTY); + this.numOfRefills = new AtomicInteger(0); + } + + public PlateState getState() { + return plateState.get(); + } + + public int getNumOfRefills() { + return numOfRefills.get(); + } + + public int getProgrammerId() { + return programmerId; + } + + public void refill() { + plateState.set(PlateState.FULL); + numOfRefills.incrementAndGet(); + } + + public void finish() { + plateState.set(PlateState.EMPTY); + } + + public void order() { + plateState.set(PlateState.ORDERED); + } + + @Override + public int compareTo(Plate o) { + return Integer.compare(this.numOfRefills.get(), o.numOfRefills.get()); + } +} diff --git a/src/main/java/org/labs/model/PlateState.java b/src/main/java/org/labs/model/PlateState.java new file mode 100644 index 0000000..a2dfca2 --- /dev/null +++ b/src/main/java/org/labs/model/PlateState.java @@ -0,0 +1,7 @@ +package org.labs.model; + +public enum PlateState { + EMPTY, + ORDERED, + FULL +} 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..6e16b3f --- /dev/null +++ b/src/main/java/org/labs/model/Programmer.java @@ -0,0 +1,81 @@ +package org.labs.model; + +import org.labs.configuration.ProgrammerProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Programmer implements Runnable { + private final Logger log = LoggerFactory.getLogger(Programmer.class); + private final ProgrammerProperties properties; + private final Restaurant restaurant; + private final Plate plate; + private final Spoon smallerSpoon; + private final Spoon biggerSpoon; + private final Integer id; + + public Programmer( + ProgrammerProperties properties, + Restaurant restaurant, + Plate plate, + Spoon smallerSpoon, + Spoon biggerSpoon, + Integer id + ) { + this.properties = properties; + this.restaurant = restaurant; + this.plate = plate; + this.smallerSpoon = smallerSpoon; + this.biggerSpoon = biggerSpoon; + this.id = id; + } + + @Override + public void run() { + try { + haveDinner(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public void haveDinner() throws InterruptedException { + while (restaurant.isServing() || plate.getState() == PlateState.FULL) { + restaurant.makeOrder(plate); + + if (plate.getState() != PlateState.FULL) { + continue; + } + + eat(); + chitChat(); + } + } + + public void eat() throws InterruptedException { + try { + smallerSpoon.take(); + log.info("Programmer with id [{}] acquired small spoon with id [{}]", id, smallerSpoon.getId()); + + biggerSpoon.take(); + log.info("Programmer with id [{}] acquired bigger spoon with id [{}]", id, biggerSpoon.getId()); + + Thread.sleep(properties.getEatingTime()); + plate.finish(); + log.info("Programmer with id [{}] finished plate number [{}]", id, plate.getNumOfRefills()); + } finally { + biggerSpoon.put(); + log.info("Programmer with id [{}] put bigger spoon with id [{}]", id, biggerSpoon.getId()); + + smallerSpoon.put(); + log.info("Programmer with id [{}] put smaller spoon with id [{}]", id, smallerSpoon.getId()); + } + } + + public void chitChat() throws InterruptedException { + Thread.sleep(properties.getChitChatTime()); + } + + public Plate getPlate() { + return plate; + } +} 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..55fc0ed --- /dev/null +++ b/src/main/java/org/labs/model/Restaurant.java @@ -0,0 +1,43 @@ +package org.labs.model; + +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Restaurant { + private final Logger log = LoggerFactory.getLogger(Restaurant.class); + private final PriorityBlockingQueue platesToServe; + private final AtomicInteger portionsLeft; + + public Restaurant(PriorityBlockingQueue platesToServe, int maxPortions) { + this.platesToServe = platesToServe; + this.portionsLeft = new AtomicInteger(maxPortions); + } + + public void makeOrder(Plate plate) { + if (plate.getState() == PlateState.EMPTY) { + plate.order(); + log.info("Programmer with id [{}] ordered new portion", plate.getProgrammerId()); + + platesToServe.add(plate); + } + } + + public boolean hasPlates() { + return !platesToServe.isEmpty(); + } + + public Plate getPlate() { + return platesToServe.poll(); + } + + public boolean takeSoup() { + return portionsLeft.getAndUpdate(portionCount -> portionCount > 0 ? portionCount - 1 : 0) > 0; + } + + public boolean isServing() { + return portionsLeft.get() > 0; + } +} 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..5791928 --- /dev/null +++ b/src/main/java/org/labs/model/Spoon.java @@ -0,0 +1,25 @@ +package org.labs.model; + +import java.util.concurrent.locks.ReentrantLock; + +public class Spoon { + private final int id; + private final ReentrantLock lock; + + public Spoon(int id) { + this.id = id; + this.lock = new ReentrantLock(); + } + + public int getId() { + return id; + } + + public void take() { + lock.lock(); + } + + public void put() { + lock.unlock(); + } +} 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..e6716cc --- /dev/null +++ b/src/main/java/org/labs/model/Waiter.java @@ -0,0 +1,51 @@ +package org.labs.model; + +import org.labs.configuration.WaiterProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Waiter implements Runnable { + private final Logger log = LoggerFactory.getLogger(Waiter.class); + private final WaiterProperties properties; + private final Restaurant restaurant; + private final int id; + + public Waiter( + WaiterProperties properties, + Restaurant restaurant, + int id + ) { + this.properties = properties; + this.restaurant = restaurant; + this.id = id; + } + + @Override + public void run() { + try { + serveProgrammers(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public void serveProgrammers() throws InterruptedException { + while (restaurant.isServing() || restaurant.hasPlates()) { + Plate plate = restaurant.getPlate(); + if (plate == null) { + continue; + } + + Thread.sleep(properties.getServingTime()); + + if (restaurant.takeSoup()) { + plate.refill(); + log.info( + "Waiter with id [{}] refilled plate for programmer with id [{}]", + id, + plate.getProgrammerId() + ); + } + } + } +} diff --git a/src/main/java/org/labs/simulation/Simulation.java b/src/main/java/org/labs/simulation/Simulation.java new file mode 100644 index 0000000..b222e89 --- /dev/null +++ b/src/main/java/org/labs/simulation/Simulation.java @@ -0,0 +1,16 @@ +package org.labs.simulation; + +import java.util.List; + +import org.labs.model.Plate; +import org.labs.model.Programmer; +import org.labs.model.Restaurant; +import org.labs.model.Waiter; + +public record Simulation( + Restaurant restaurant, + List programmers, + List waiters, + List plates +) { +} \ No newline at end of file diff --git a/src/main/java/org/labs/simulation/SimulationUtils.java b/src/main/java/org/labs/simulation/SimulationUtils.java new file mode 100644 index 0000000..b96c830 --- /dev/null +++ b/src/main/java/org/labs/simulation/SimulationUtils.java @@ -0,0 +1,106 @@ +package org.labs.simulation; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.labs.configuration.SimulationConfig; +import org.labs.model.Plate; +import org.labs.model.Programmer; +import org.labs.model.Restaurant; +import org.labs.model.Spoon; +import org.labs.model.Waiter; +import org.labs.statistics.SimulationStatistics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SimulationUtils { + private final static Logger log = LoggerFactory.getLogger(SimulationUtils.class); + + public static Simulation createSimulation(SimulationConfig config) { + PriorityBlockingQueue platesToServe = new PriorityBlockingQueue<>(); + + Restaurant restaurant = new Restaurant(platesToServe, config.totalFoodPortions()); + + List spoons = new ArrayList<>(); + for (int i = 0; i < config.numberOfProgrammers(); i++) { + spoons.add(new Spoon(i)); + } + + List plates = new ArrayList<>(); + List programmers = new ArrayList<>(); + + for (int i = 0; i < config.numberOfProgrammers(); i++) { + Plate plate = new Plate(i); + plates.add(plate); + + Spoon leftSpoon = spoons.get(i); + Spoon rightSpoon = spoons.get((i + 1) % config.numberOfProgrammers()); + Spoon smallerSpoon = leftSpoon.getId() < rightSpoon.getId() ? leftSpoon : rightSpoon; + Spoon biggerSpoon = leftSpoon.getId() > rightSpoon.getId() ? leftSpoon : rightSpoon; + + Programmer programmer = new Programmer( + config.programmerProperties(), + restaurant, + plate, + smallerSpoon, + biggerSpoon, + i + ); + programmers.add(programmer); + } + + List waiters = new ArrayList<>(); + for (int i = 0; i < config.numberOfWaiters(); i++) { + Waiter waiter = new Waiter(config.waiterProperties(), restaurant, i); + waiters.add(waiter); + } + + return new Simulation(restaurant, programmers, waiters, plates); + } + + public static SimulationStatistics runSimulation( + Simulation simulation, + SimulationConfig config + ) throws InterruptedException { + try ( + ExecutorService programmerPool = Executors.newVirtualThreadPerTaskExecutor(); + ExecutorService waiterPool = Executors.newVirtualThreadPerTaskExecutor(); +// ExecutorService programmerPool = Executors.newFixedThreadPool(config.numberOfProgrammers()); +// ExecutorService waiterPool = Executors.newFixedThreadPool(config.numberOfProgrammers()) + ) { + long startTime = System.currentTimeMillis(); + + log.info( + "Starting simulation with [{}] programmers and [{}] waiters", + config.numberOfProgrammers(), + config.numberOfWaiters() + ); + + try { + for (Programmer programmer : simulation.programmers()) { + programmerPool.submit(programmer); + } + for (Waiter waiter : simulation.waiters()) { + waiterPool.submit(waiter); + } + } finally { + programmerPool.shutdown(); + waiterPool.shutdown(); + + while (!programmerPool.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("Waiting for programmers pool to terminate"); + } + + while (!waiterPool.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("Waiting for waiters pool to terminate"); + } + } + + return new SimulationStatistics(startTime, simulation.plates()); + } + } +} diff --git a/src/main/java/org/labs/statistics/SimulationStatistics.java b/src/main/java/org/labs/statistics/SimulationStatistics.java new file mode 100644 index 0000000..798a759 --- /dev/null +++ b/src/main/java/org/labs/statistics/SimulationStatistics.java @@ -0,0 +1,70 @@ +package org.labs.statistics; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.labs.model.Plate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SimulationStatistics { + private static final Logger logger = LoggerFactory.getLogger(SimulationStatistics.class); + + private final Map programmerPortions; + private final long totalTime; + + private final int totalRefills; + private final int minPortions; + private final int maxPortions; + private final double fairnessRatio; + + public SimulationStatistics(Long startTime, List plates) { + this.totalTime = System.currentTimeMillis() - startTime; + this.totalRefills = plates.stream() + .mapToInt(Plate::getNumOfRefills) + .sum(); + + this.programmerPortions = plates.stream() + .collect(Collectors.toMap( + Plate::getProgrammerId, + Plate::getNumOfRefills + )); + this.minPortions = programmerPortions.values().stream() + .min(Integer::compare) + .orElse(0); + this.maxPortions = programmerPortions.values().stream() + .max(Integer::compare) + .orElse(0); + + this.fairnessRatio = (double) minPortions / maxPortions * 100; + } + + public void printFinalStatistics(List plates) { + logger.info("=== SIMULATION STATISTICS ==="); + logger.info("Total simulation time: {} ms ({} seconds)", totalTime, totalTime / 1000.0); + logger.info("Total plate refills: {}", totalRefills); + + logger.info("=== PROGRAMMER STATISTICS ==="); + + programmerPortions.forEach((key, value) -> logger.info("Programmer {} ate {} portions", key, value)); + + logger.info("=== FAIRNESS ANALYSIS ==="); + logger.info("Total refills from plates: {}", totalRefills); + logger.info("Min portions eaten: {}", minPortions); + logger.info("Max portions eaten: {}", maxPortions); + logger.info("Difference: {} portions", maxPortions - minPortions); + logger.info( + "Fairness ratio: {}%", + minPortions > 0 ? fairnessRatio : 0 + ); + } + + public int getTotalRefills() { + return totalRefills; + } + + public double getFairnessRation() { + return fairnessRatio; + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..ee88e97 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,40 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + logs/simulation.log + + + logs/simulation.%d{yyyy-MM-dd}.%i.log + 10MB + 30 + 1GB + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + 1024 + 0 + false + + + + + + + + + + + diff --git a/src/main/resources/simulation-config.json b/src/main/resources/simulation-config.json new file mode 100644 index 0000000..8c82f5d --- /dev/null +++ b/src/main/resources/simulation-config.json @@ -0,0 +1,15 @@ +{ + "numberOfProgrammers": 7, + "numberOfWaiters": 2, + "totalFoodPortions": 1000000, + "programmerProperties": { + "minEatingTime": 1, + "maxEatingTime": 2, + "minChitChatTime": 3, + "maxChitChatTime": 4 + }, + "waiterProperties": { + "minServingTime": 1, + "maxServingTime": 2 + } +} diff --git a/src/test/java/org/labs/SimulationTest.java b/src/test/java/org/labs/SimulationTest.java new file mode 100644 index 0000000..0e6ebe8 --- /dev/null +++ b/src/test/java/org/labs/SimulationTest.java @@ -0,0 +1,43 @@ +package org.labs; + +import org.junit.jupiter.api.Test; +import org.labs.configuration.ConfigLoader; +import org.labs.configuration.SimulationConfig; +import org.labs.model.Plate; +import org.labs.simulation.Simulation; +import org.labs.statistics.SimulationStatistics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.labs.simulation.SimulationUtils.createSimulation; +import static org.labs.simulation.SimulationUtils.runSimulation; + +class SimulationTest { + + @Test + void testTotalPortionsEaten() throws Exception { + SimulationConfig config = ConfigLoader.loadConfig("test-config.json"); + + Simulation simulation = createSimulation(config); + + SimulationStatistics statistics = runSimulation(simulation, config); + + assertEquals(config.totalFoodPortions(), statistics.getTotalRefills(), + "Total refills should equal total food portions"); + + for (Plate plate : simulation.plates()) { + assertTrue(plate.getNumOfRefills() > 0, + "Each programmer should have eaten at least one portion"); + } + + + assertTrue( + statistics.getFairnessRation() >= 0.95d, + String.format( + "Fairness check failed: fairness ratio should be >= 0.95, but was {%s}", + statistics.getFairnessRation() + ) + ); + } +} + diff --git a/src/test/resources/test-config.json b/src/test/resources/test-config.json new file mode 100644 index 0000000..31d7a8a --- /dev/null +++ b/src/test/resources/test-config.json @@ -0,0 +1,15 @@ +{ + "numberOfProgrammers": 7, + "numberOfWaiters": 2, + "totalFoodPortions": 100, + "programmerProperties": { + "minEatingTime": 5, + "maxEatingTime": 10, + "minChitChatTime": 7, + "maxChitChatTime": 9 + }, + "waiterProperties": { + "minServingTime": 5, + "maxServingTime": 10 + } +} \ No newline at end of file