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..a912783 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,15 +1,24 @@ plugins { id("java") + id("io.freefair.lombok") version "8.13.1" } group = "org.labs" version = "1.0-SNAPSHOT" +java { + sourceCompatibility = JavaVersion.VERSION_21 +} + repositories { mavenCentral() } dependencies { + implementation("org.slf4j:slf4j-api:2.0.17") + implementation("ch.qos.logback:logback-classic:1.5.18") + implementation("org.projectlombok:lombok:1.18.42") + testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") } 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..8df915b 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -1,7 +1,45 @@ package org.labs; +import lombok.extern.slf4j.Slf4j; +import org.labs.io.ParamsReader; +import org.labs.service.FoodService; +import org.labs.service.QueueService; + +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.labs.configuration.ConfigurationParam.EAT_COUNT; +import static org.labs.configuration.ConfigurationParam.PROGRAMMERS_COUNT; +import static org.labs.configuration.ConfigurationParam.WAITERS_COUNT; + +@Slf4j public class Main { public static void main(String[] args) { - System.out.println("Hello, World!"); + var reader = new ParamsReader(); + + var params = reader.getParamsAsMap("src/main/resources/params"); + var eatCount = params.get(EAT_COUNT); + + var queueService = new QueueService(); + var foodService = new FoodService(eatCount); + + var waiters = Executors.newFixedThreadPool(params.get(WAITERS_COUNT)); + var programmers = Executors.newFixedThreadPool(params.get(PROGRAMMERS_COUNT)); + + var simulation = new Simulation(params, queueService, foodService); + simulation.run(waiters, programmers); + + waiters.shutdown(); + programmers.shutdown(); + try { + waiters.awaitTermination(60, TimeUnit.SECONDS); + programmers.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + log.info("Симуляция завершена. Сколько поел каждый:"); + foodService.getProgrammerIdToSoupCount().forEach((id, count) -> + log.info("Программист {} поел {} раз", id, count)); } } \ No newline at end of file diff --git a/src/main/java/org/labs/Simulation.java b/src/main/java/org/labs/Simulation.java new file mode 100644 index 0000000..137cb22 --- /dev/null +++ b/src/main/java/org/labs/Simulation.java @@ -0,0 +1,67 @@ +package org.labs; + +import lombok.extern.slf4j.Slf4j; +import org.labs.configuration.ConfigurationParam; +import org.labs.model.Fork; +import org.labs.model.Programmer; +import org.labs.model.Waiter; +import org.labs.service.FoodService; +import org.labs.service.QueueService; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.locks.ReentrantLock; + +import static org.labs.configuration.ConfigurationParam.PROGRAMMERS_COUNT; +import static org.labs.configuration.ConfigurationParam.TIMEOUT_MS; +import static org.labs.configuration.ConfigurationParam.WAITERS_COUNT; + +@Slf4j +public class Simulation { + private final Map params; + private final QueueService queueService; + private final FoodService foodService; + + private final List forks; + + public Simulation(Map params, QueueService queueService, FoodService foodService) { + this.params = params; + this.queueService = queueService; + this.foodService = foodService; + this.forks = new ArrayList<>(); + } + + public void run(ExecutorService waiters, ExecutorService programmers) { + log.info("Начали симуляцию"); + foodService.initSoups(params.get(PROGRAMMERS_COUNT)); + fillForks(params.get(PROGRAMMERS_COUNT)); + fillProgrammers(params.get(PROGRAMMERS_COUNT), programmers, params.get(TIMEOUT_MS)); + createWaiters(params.get(WAITERS_COUNT), waiters, params.get(TIMEOUT_MS)); + } + + private void fillForks(int forksCount) { + for (int i = 0; i < forksCount; i++) { + forks.add(new Fork(i + 1, new ReentrantLock())); + } + } + + private void fillProgrammers(int programmersCount, ExecutorService executor, long timeoutMs) { + for (int i = 0; i < programmersCount; i++) { + var programmer = new Programmer(i + 1, foodService, queueService, timeoutMs); + programmer.setState(Programmer.State.HUNGRY); + var left = forks.get(i); + var right = forks.get((i + 1) % programmersCount); + programmer.setLeft(left.id() < right.id() ? left : right); + programmer.setRight(left.id() < right.id() ? right : left); + executor.submit(programmer); + } + } + + private void createWaiters(int waitersCount, ExecutorService executor, long timeoutMs) { + for (int i = 0; i < waitersCount; i++) { + executor.submit(new Waiter(i + 1, queueService, foodService, timeoutMs)); + } + } +} diff --git a/src/main/java/org/labs/configuration/ConfigurationParam.java b/src/main/java/org/labs/configuration/ConfigurationParam.java new file mode 100644 index 0000000..49ec189 --- /dev/null +++ b/src/main/java/org/labs/configuration/ConfigurationParam.java @@ -0,0 +1,27 @@ +package org.labs.configuration; + +import lombok.Getter; + +@Getter +public enum ConfigurationParam { + PROGRAMMERS_COUNT("programmers_count"), + EAT_COUNT("eat_count"), + WAITERS_COUNT("waiters_count"), + TIMEOUT_MS("timeout_ms"), + ; + + private final String value; + + ConfigurationParam(String value) { + this.value = value; + } + + public static ConfigurationParam fromValue(String value) { + for (ConfigurationParam param : values()) { + if (param.value.equals(value)) { + return param; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/src/main/java/org/labs/io/ParamsReader.java b/src/main/java/org/labs/io/ParamsReader.java new file mode 100644 index 0000000..48c0e4f --- /dev/null +++ b/src/main/java/org/labs/io/ParamsReader.java @@ -0,0 +1,26 @@ +package org.labs.io; + +import org.labs.configuration.ConfigurationParam; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; +import java.util.stream.Collectors; + +public class ParamsReader { + private static final String SEPARATOR = "="; + + public Map getParamsAsMap(String filePath) { + try (var lines = Files.lines(Paths.get(filePath))) { + return lines + .map(line -> line.split(SEPARATOR)) + .collect(Collectors.toMap( + values -> ConfigurationParam.fromValue(values[0]), + values -> Integer.parseInt(values[1]) + )); + } catch (IOException e) { + throw new RuntimeException("Can't read file " + filePath, e); + } + } +} diff --git a/src/main/java/org/labs/model/Fork.java b/src/main/java/org/labs/model/Fork.java new file mode 100644 index 0000000..8a49aa2 --- /dev/null +++ b/src/main/java/org/labs/model/Fork.java @@ -0,0 +1,21 @@ +package org.labs.model; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.locks.Lock; + +@Slf4j +public record Fork( + int id, + Lock lock +) { + public void pickUp(int programmerId) { + lock.lock(); + log.info("Программист {} взял вилку с id {}", programmerId, id); + } + + public void pickDown(int programmerId) { + lock.unlock(); + log.info("Программист {} положил вилку с id {}", programmerId, id); + } +} 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..187fdc3 --- /dev/null +++ b/src/main/java/org/labs/model/Programmer.java @@ -0,0 +1,86 @@ +package org.labs.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.labs.service.FoodService; +import org.labs.service.QueueService; + +@Getter +@RequiredArgsConstructor +@Slf4j +public class Programmer implements Runnable { + private final int id; + @Setter + private State state; + @Setter + private Fork left; + @Setter + private Fork right; + + private final FoodService foodService; + private final QueueService queueService; + private final long timeoutMs; + + @Override + @SneakyThrows + public void run() { + while (foodService.getEatCount().get() > 0) { + this.state = State.HUNGRY; + log.info("Программист: {}, осталось еды - {}", id, foodService.getEatCount()); + + while (!foodService.hasSoup(id)) { + if (foodService.getEatCount().get() < 1) { + break; + } + log.info("Нет супа, программист с id = {} не может поесть. Он будет ждать, когда пополнится порция", id); + if (queueService.contains(id)) { + log.warn("Программист {} уже в очереди", id); + } + if (!queueService.contains(id) && foodService.getEatCount().get() > 0) { + queueService.put(id); + } + + Thread.sleep(timeoutMs); + // сказать, что нет супа и дождаться, пока он появиться, то есть кинуть поток в сон + } + + if (foodService.hasSoup(id) && foodService.getEatCount().get() > 0) { + try { + left.pickUp(id); + right.pickUp(id); + + state = State.EATING; + + log.info("Программист {} начал кушать суп", id); + + foodService.disableSoup(id); + + } finally { + log.info("Программист {} закончил есть суп и собирается положить вилки", id); + right.pickDown(id); + left.pickDown(id); + + log.info("Программист {} положил вилки", id); + } + + log.info("Программист {} начал разговаривать", id); + + state = State.TALKING; + Thread.sleep(timeoutMs); + } + } + // он должен думать + // посмотреть, есть ли у него суп + // если нет - попросить официанта налить и продолжить думать + // если есть - взять ложку раз, взять ложку два, начать есть суп (если все съел - отпустить ложки и начать думать) + } + + public enum State { + TALKING, + EATING, + HUNGRY; + } +} 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..500aa15 --- /dev/null +++ b/src/main/java/org/labs/model/Waiter.java @@ -0,0 +1,34 @@ +package org.labs.model; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.labs.service.FoodService; +import org.labs.service.QueueService; + +@RequiredArgsConstructor +@Slf4j +public class Waiter implements Runnable { + private final int id; + private final QueueService queueService; + private final FoodService foodService; + private final long timeoutMs; + + @Override + @SneakyThrows + public void run() { + while (foodService.getEatCount().get() > 0) { + log.info("Официант {}, его очередь = {}", id, queueService.print()); + var programmerId = queueService.poll(); + log.info("Официант {} взял программиста с id = {}", id, programmerId); + if (programmerId != null) { + if (foodService.addSoupToProgrammer(programmerId)) { + log.info("Официант {} добавил программисту {} суп", id, programmerId); + } else { + break; + } + } + Thread.sleep(timeoutMs); + } + } +} diff --git a/src/main/java/org/labs/service/FoodService.java b/src/main/java/org/labs/service/FoodService.java new file mode 100644 index 0000000..284a5ec --- /dev/null +++ b/src/main/java/org/labs/service/FoodService.java @@ -0,0 +1,58 @@ +package org.labs.service; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Getter +@Slf4j +public class FoodService { + private final AtomicInteger eatCount; + private final Map programmerIdToSoupAvailable = new ConcurrentHashMap<>(); + private final Map programmerIdToSoupCount = new ConcurrentHashMap<>(); + private final Lock lock = new ReentrantLock(); + + public FoodService(int eatCount) { + this.eatCount = new AtomicInteger(eatCount); + } + + public void incrementSoupCountByProgrammerId(int programmerId) { + this.programmerIdToSoupCount.put( + programmerId, + programmerIdToSoupCount.getOrDefault(programmerId, 0) + 1 + ); + } + + public void initSoups(int programmersCount) { + for (int i = 0; i < programmersCount; i++) { + programmerIdToSoupAvailable.put(i + 1, false); + } + } + + public boolean hasSoup(int programmerId) { + return programmerIdToSoupAvailable.get(programmerId); + } + + public boolean addSoupToProgrammer(int programmerId) { + if (eatCount.get() == 0) { + return false; + } + eatCount.decrementAndGet(); + programmerIdToSoupAvailable.put(programmerId, true); + log.info("Программист {} получил порцию супа. Осталось порций: {}", programmerId, eatCount.get()); + incrementSoupCountByProgrammerId(programmerId); + getProgrammerIdToSoupCount().forEach((id, count) -> + log.warn("Программист {} поел {} раз", id, count)); + return true; + } + + public void disableSoup(int programmerId) { + log.info("Программист {} съел порцию супа", programmerId); + programmerIdToSoupAvailable.put(programmerId, false); + } +} diff --git a/src/main/java/org/labs/service/QueueService.java b/src/main/java/org/labs/service/QueueService.java new file mode 100644 index 0000000..7ac70ba --- /dev/null +++ b/src/main/java/org/labs/service/QueueService.java @@ -0,0 +1,30 @@ +package org.labs.service; + +import lombok.Getter; + +import java.util.concurrent.ConcurrentLinkedQueue; + +@Getter +public class QueueService { + private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + + public void put(Integer programmerId) { + this.queue.add(programmerId); + } + + public Integer poll() { + if (this.queue.isEmpty()) { + return null; + } + + return queue.poll(); + } + + public boolean contains(Integer programmerId) { + return this.queue.contains(programmerId); + } + + public String print() { + return this.queue.toString(); + } +} diff --git a/src/main/resources/params b/src/main/resources/params new file mode 100644 index 0000000..33c3e1f --- /dev/null +++ b/src/main/resources/params @@ -0,0 +1,4 @@ +programmers_count=3 +eat_count=3000 +waiters_count=7 +timeout_ms=10