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..cbaf84a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,10 @@ repositories { } dependencies { + implementation("org.slf4j:slf4j-api:2.0.16") + implementation("ch.qos.logback:logback-classic:1.5.13") + compileOnly("org.projectlombok:lombok:1.18.36") + annotationProcessor("org.projectlombok:lombok:1.18.36") testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") } diff --git a/src/main/java/org/labs/DinnerProcessingService.java b/src/main/java/org/labs/DinnerProcessingService.java new file mode 100644 index 0000000..0b5a5cb --- /dev/null +++ b/src/main/java/org/labs/DinnerProcessingService.java @@ -0,0 +1,106 @@ +package org.labs; + +import lombok.extern.slf4j.Slf4j; +import org.labs.common.AsyncUtils; +import org.labs.config.Config; +import org.labs.developer.model.DeveloperModel; +import org.labs.spoon.model.SpoonModel; +import org.labs.kitchen.model.KitchenModel; +import org.labs.state.model.StateModel; +import org.labs.waiter.model.WaiterModel; + +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +@Slf4j +public class DinnerProcessingService { + + private static final int TARGET_RESOURCE_COUNT = Config.DEVELOPER_COUNT; + private static final int DISH_COUNT = Config.DISH_COUNT; + private static final int WAITER_COUNT = Config.WAITER_COUNT; + + private final ExecutorService developerPool = Executors.newWorkStealingPool(TARGET_RESOURCE_COUNT); + private final ExecutorService waiterPool = Executors.newWorkStealingPool(WAITER_COUNT); + + public DeveloperModel[] runDinner() { + var startDate = System.currentTimeMillis(); + log.info("Время {} мс. Обед начался", startDate); + + var kitchen = new KitchenModel(new AtomicInteger(DISH_COUNT)); + var spoons = createSpoons(); + var developers = createDevelopers(spoons, kitchen); + var waiters = createWaiters(kitchen); + + startDeveloperTasks(developers); + startWaiterTasks(waiters); + + monitorDinner(kitchen); + + stopDinner(); + + log.info("Время выполнения: {} мс. Обед завершен", System.currentTimeMillis() - startDate); + + return developers; + } + + private SpoonModel[] createSpoons() { + return IntStream.range(0, TARGET_RESOURCE_COUNT) + .mapToObj(SpoonModel::new) + .toArray(SpoonModel[]::new); + } + + private DeveloperModel[] createDevelopers(SpoonModel[] spoons, KitchenModel kitchen) { + var developers = new DeveloperModel[TARGET_RESOURCE_COUNT]; + var state = new StateModel(developers); + + IntStream.range(0, developers.length).forEach(number -> { + var leftSpoon = spoons[number]; + var rightSpoon = spoons[(number + 1) % spoons.length]; + developers[number] = new DeveloperModel(number, leftSpoon, rightSpoon, state, kitchen); + }); + return developers; + } + + private WaiterModel[] createWaiters(KitchenModel kitchen) { + var waiters = new WaiterModel[WAITER_COUNT]; + IntStream.range(0, WAITER_COUNT).forEach(number -> waiters[number] = new WaiterModel(number, kitchen)); + return waiters; + } + + private void startDeveloperTasks(DeveloperModel[] developers) { + Arrays.stream(developers).forEach(developerPool::submit); + } + + private void startWaiterTasks(WaiterModel[] waiters) { + Arrays.stream(waiters).forEach(waiterPool::submit); + } + + private void monitorDinner(KitchenModel kitchen) { + while (kitchen.getRemainingDishCount() > 0) { + AsyncUtils.waitMillis(200); + log.debug("Оставшееся количество блюд на кухне {}", kitchen.getRemainingDishCount()); + } + } + + private void stopDinner() { + AsyncUtils.waitMillis(Config.DINNER_DURATION_IN_MS); + shutdownAndAwaitTermination(developerPool); + shutdownAndAwaitTermination(waiterPool); + } + + private void shutdownAndAwaitTermination(ExecutorService pool) { + pool.shutdown(); + try { + if (!pool.awaitTermination(5, TimeUnit.SECONDS)) { + pool.shutdownNow(); + } + } catch (InterruptedException e) { + pool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/java/org/labs/Main.java b/src/main/java/org/labs/Main.java index 9917247..949c99e 100644 --- a/src/main/java/org/labs/Main.java +++ b/src/main/java/org/labs/Main.java @@ -1,7 +1,38 @@ package org.labs; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.labs.common.MathUtils; +import org.labs.developer.model.DeveloperModel; + +import java.util.Arrays; +import java.util.stream.IntStream; + +@Slf4j +@RequiredArgsConstructor public class Main { + + private static final DinnerProcessingService dinnerProcessingService = new DinnerProcessingService(); + public static void main(String[] args) { - System.out.println("Hello, World!"); + var developers = dinnerProcessingService.runDinner(); + printStates(developers); + } + + private static void printStates(DeveloperModel[] developers) { + int totalCount = Arrays.stream(developers) + .mapToInt(developer -> developer.getEatCount().intValue()) + .sum(); + + if (totalCount > 0) { + log.info("Итоговое состояние:"); + log.info("Всего съедено: {}", totalCount); + + IntStream.range(0, developers.length) + .forEachOrdered(i -> { + var value = 100.0 * developers[i].getEatCount().intValue() / totalCount; + log.info("Разработчик {} съел {}% блюд", i + 1, MathUtils.roundTo2Digits(value)); + }); + } } } \ No newline at end of file diff --git a/src/main/java/org/labs/common/AsyncUtils.java b/src/main/java/org/labs/common/AsyncUtils.java new file mode 100644 index 0000000..401bd55 --- /dev/null +++ b/src/main/java/org/labs/common/AsyncUtils.java @@ -0,0 +1,22 @@ +package org.labs.common; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ThreadLocalRandom; + +@Slf4j +public class AsyncUtils { + + public static void waitMillis(long maxMillis) { + if (maxMillis <= 0) { + return; + } + try { + long delay = ThreadLocalRandom.current().nextLong(maxMillis + 1); + Thread.sleep(delay); + } catch (InterruptedException e) { + log.warn("Поток {} был прерван", Thread.currentThread().getName()); + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/java/org/labs/common/MathUtils.java b/src/main/java/org/labs/common/MathUtils.java new file mode 100644 index 0000000..b59564a --- /dev/null +++ b/src/main/java/org/labs/common/MathUtils.java @@ -0,0 +1,14 @@ +package org.labs.common; + +import org.labs.config.Config; + +public class MathUtils { + + public static double roundTo2Digits(double value) { + return Math.round(value * 100) / 100.0; + } + + public static int getRandomInt() { + return (int) (Math.random() * Config.MAX_WAIT_MS); + } +} diff --git a/src/main/java/org/labs/config/Config.java b/src/main/java/org/labs/config/Config.java new file mode 100644 index 0000000..fc18f38 --- /dev/null +++ b/src/main/java/org/labs/config/Config.java @@ -0,0 +1,13 @@ +package org.labs.config; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class Config { + public static int DEVELOPER_COUNT = 7; + public static int MAX_WAIT_MS = 1; + public static int DINNER_DURATION_IN_MS = 10; + public static int DISH_COUNT = 1000000; + public static int WAITER_COUNT = 2; +} diff --git a/src/main/java/org/labs/developer/model/DeveloperModel.java b/src/main/java/org/labs/developer/model/DeveloperModel.java new file mode 100644 index 0000000..e9540e0 --- /dev/null +++ b/src/main/java/org/labs/developer/model/DeveloperModel.java @@ -0,0 +1,77 @@ +package org.labs.developer.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.labs.common.MathUtils; +import org.labs.spoon.model.SpoonModel; +import org.labs.kitchen.model.KitchenModel; +import org.labs.serverequest.model.ServeRequest; +import org.labs.state.model.StateModel; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +@Getter +@RequiredArgsConstructor +public class DeveloperModel implements Runnable { + + private final int id; + private final SpoonModel leftSpoon; + private final SpoonModel rightSpoon; + private final StateModel state; + private final KitchenModel kitchen; + + public final AtomicBoolean isStopped = new AtomicBoolean(); + + public AtomicInteger eatCount = new AtomicInteger(); + + @Override + public void run() { + try { + while (!isStopped.get()) { + think(); + var served = placeOrder(); + if (!served) { + isStopped.set(true); + break; + } + + state.takeSpoons(id, leftSpoon, rightSpoon); + eat(); + state.putSpoons(id, leftSpoon, rightSpoon); + } + } catch (InterruptedException ignored) { + log.warn("Поток {} был прерван", Thread.currentThread().getName()); + } + } + + private void think() throws InterruptedException { + log.debug("Время: {} ms. Разработчик начал обсуждать преподавателей", System.currentTimeMillis()); + Thread.sleep(MathUtils.getRandomInt()); + } + + private void eat() throws InterruptedException { + log.debug("Время: {} ms. Разработчик начал есть", System.currentTimeMillis()); + Thread.sleep(MathUtils.getRandomInt()); + eatCount.incrementAndGet(); + } + + private boolean placeOrder() throws InterruptedException { + try { + var serveRequest = new ServeRequest(id); + kitchen.submitRequest(serveRequest); + var isServed = serveRequest.getServed().get(); + if (!isServed) { + log.info("Разработчик {} не может получить новую порцию. Его обед завершен", id + 1); + return false; + } + } catch (ExecutionException e) { + log.warn("Разработчик {} не смог вызвать официанта", id + 1, e); + return false; + } + return true; + } +} diff --git a/src/main/java/org/labs/kitchen/model/KitchenModel.java b/src/main/java/org/labs/kitchen/model/KitchenModel.java new file mode 100644 index 0000000..ba05d0a --- /dev/null +++ b/src/main/java/org/labs/kitchen/model/KitchenModel.java @@ -0,0 +1,45 @@ +package org.labs.kitchen.model; + +import lombok.RequiredArgsConstructor; +import org.labs.serverequest.model.ServeRequest; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +@RequiredArgsConstructor +public class KitchenModel { + + private final AtomicInteger remainingDishCount; + private final BlockingQueue requestQueue = new PriorityBlockingQueue<>(); + + public void submitRequest(ServeRequest req) throws InterruptedException { + requestQueue.put(req); + } + + public ServeRequest takeRequest() throws InterruptedException { + return requestQueue.take(); + } + + public boolean takeDishIfAvailable() { + int currentRemainingDishCount; + do { + currentRemainingDishCount = remainingDishCount.get(); + if (currentRemainingDishCount <= 0) return false; + } while (!remainingDishCount.compareAndSet(currentRemainingDishCount, currentRemainingDishCount - 1)); + return true; + } + + public int getRemainingDishCount() { + return remainingDishCount.get(); + } + + public boolean isDepleted() { + return remainingDishCount.get() <= 0; + } + + public boolean isRequestQueueEmpty() { + return requestQueue.isEmpty(); + } + +} diff --git a/src/main/java/org/labs/serverequest/model/ServeRequest.java b/src/main/java/org/labs/serverequest/model/ServeRequest.java new file mode 100644 index 0000000..6f13ab5 --- /dev/null +++ b/src/main/java/org/labs/serverequest/model/ServeRequest.java @@ -0,0 +1,19 @@ +package org.labs.serverequest.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.concurrent.CompletableFuture; + +@Getter +@RequiredArgsConstructor +public class ServeRequest implements Comparable { + private final int developerId; + private final long timestamp = System.nanoTime(); + private final CompletableFuture served = new CompletableFuture<>(); + + @Override + public int compareTo(ServeRequest other) { + return Long.compare(this.timestamp, other.timestamp); + } +} diff --git a/src/main/java/org/labs/spoon/model/SpoonModel.java b/src/main/java/org/labs/spoon/model/SpoonModel.java new file mode 100644 index 0000000..dde217c --- /dev/null +++ b/src/main/java/org/labs/spoon/model/SpoonModel.java @@ -0,0 +1,14 @@ +package org.labs.spoon.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + + +@Getter +@RequiredArgsConstructor +public class SpoonModel { + private final int id; + @Setter + private boolean isAvailable = true; +} diff --git a/src/main/java/org/labs/state/model/State.java b/src/main/java/org/labs/state/model/State.java new file mode 100644 index 0000000..5282c02 --- /dev/null +++ b/src/main/java/org/labs/state/model/State.java @@ -0,0 +1,11 @@ +package org.labs.state.model; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public enum State { + HUNGRY, + EATING, + DISCUSS_TEACHERS +} diff --git a/src/main/java/org/labs/state/model/StateModel.java b/src/main/java/org/labs/state/model/StateModel.java new file mode 100644 index 0000000..1fdb83c --- /dev/null +++ b/src/main/java/org/labs/state/model/StateModel.java @@ -0,0 +1,88 @@ +package org.labs.state.model; + +import lombok.extern.slf4j.Slf4j; +import org.labs.developer.model.DeveloperModel; +import org.labs.spoon.model.SpoonModel; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Slf4j +public class StateModel { + + private final int developerCount; + private final DeveloperModel[] developers; + private final State[] state; + private final Lock lock = new ReentrantLock();; + private final Condition[] conditions; + + public StateModel(DeveloperModel[] developers) { + this.developers = developers; + this.developerCount = developers.length; + state = new State[developerCount]; + conditions = new Condition[developerCount]; + IntStream.range(0, developerCount).forEach(i -> { + state[i] = State.DISCUSS_TEACHERS; + conditions[i] = lock.newCondition(); + }); + } + + public void takeSpoons(int id, SpoonModel leftSpoon, SpoonModel rightSpoon) { + lock.lock(); + try { + updateDeveloperState(id, State.HUNGRY); + + while (!developers[id].getIsStopped().get() && (!leftSpoon.isAvailable() || !rightSpoon.isAvailable())) { + conditions[id].await(); + } + + leftSpoon.setAvailable(false); + rightSpoon.setAvailable(false); + + updateDeveloperState(id, State.EATING); + + printState(); + } catch (InterruptedException e) { + log.warn("Поток {} был прерван", Thread.currentThread().getName()); + } finally { + lock.unlock(); + } + } + + + public void putSpoons(int developerId, SpoonModel leftSpoon, SpoonModel rightSpoon) { + lock.lock(); + try { + updateDeveloperState(developerId, State.DISCUSS_TEACHERS); + + leftSpoon.setAvailable(true); + rightSpoon.setAvailable(true); + + conditions[(developerId + 1) % developerCount].signalAll(); + conditions[(developerId + developerCount - 1) % developerCount].signalAll(); + + printState(); + } finally { + lock.unlock(); + } + } + + private void updateDeveloperState(int id, State state) { + this.state[id] = state; + } + + private void printState() { + String result = IntStream.range(0, developerCount) + .mapToObj(i -> switch (state[i]) { + case DISCUSS_TEACHERS -> "обсуждает преподавателей"; + case HUNGRY -> "голоден"; + case EATING -> "ест"; + }) + .collect(Collectors.joining(" ", "Сводка по занятости разработчиков: ", "")); + + log.debug("{}", result); + } +} diff --git a/src/main/java/org/labs/waiter/model/WaiterModel.java b/src/main/java/org/labs/waiter/model/WaiterModel.java new file mode 100644 index 0000000..15e59eb --- /dev/null +++ b/src/main/java/org/labs/waiter/model/WaiterModel.java @@ -0,0 +1,36 @@ +package org.labs.waiter.model; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.labs.kitchen.model.KitchenModel; + +@Slf4j +@RequiredArgsConstructor +public class WaiterModel implements Runnable { + + private final int id; + private final KitchenModel kitchen; + + @Override + public void run() { + try { + while (!(kitchen.isDepleted() && kitchen.isRequestQueueEmpty())) { + var serveRequest = kitchen.takeRequest(); + if (serveRequest == null) { + continue; + } + + var isServed = kitchen.takeDishIfAvailable(); + log.debug("Официант {} начал обслуживание разработчика {} -> {}", + id + 1, serveRequest.getDeveloperId() + 1, isServed); + + serveRequest.getServed().complete(isServed); + } + + log.info("Официант {} разнес все блюда, заказы закончились", id + 1); + } catch (InterruptedException e) { + log.warn("Поток {} был прерван", Thread.currentThread().getName()); + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..3526d48 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + [%d{HH:mm:ss}] [%thread] %-5level %msg%n + + + + + + + \ No newline at end of file