Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 100 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,125 @@
[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/qcWcnElX)

# Java concurrency

# Цели и задачи л/р:

Задача об обедающих философах:

Рассмотрим семь программистов, сидящих вокруг круглого стола для обеда.
У каждого программиста есть тарелка супа перед ним, а между каждой парой программистов находится ложка.
Однако, чтобы поесть суп, программисту необходимо взять две ложки - справа и слева (он очень голодный).
Когда программист поедает суп, ложки остаются занятыми и не могут быть использованы соседними программистами.
Программисты чередуют прием еды с обсуждением преподавателей.
Когда суп заканчивается, программист просит одного из двух официантов принести ему еще одну порцию (то есть тарелка супа ограничена).
Когда суп заканчивается, программист просит одного из двух официантов принести ему еще одну порцию (то есть тарелка супа
ограничена).
Всего в ресторане есть 1_000_000 порций еды, после чего обед заканчивается.
Все программисты должны поесть +- одинаково, чтобы никому не было обидно



Ваша задача - реализовать симуляцию обеда с использованием языка программирования Java и многопоточности.
Каждый программист должен быть представлен в виде потока, а ложки - в виде общих ресурсов, которые программисты могут захватывать и освобождать.
Каждый программист должен быть представлен в виде потока, а ложки - в виде общих ресурсов, которые программисты могут
захватывать и освобождать.
Также не забудьте про официантов и запасы еды.

Дополнительное условие -- количество программистов, еды и официантов должно быть параметризируемое.

[Это усложнение классической задачи, про которую можно почитать тут](https://en.wikipedia.org/wiki/Dining_philosophers_problem)

Необходимо обеспечить корректное выполнение программы, чтобы избежать состояний взаимной блокировки и гарантировать, что каждый программист получит возможность поесть.
Необходимо обеспечить корректное выполнение программы, чтобы избежать состояний взаимной блокировки и гарантировать, что
каждый программист получит возможность поесть.

# Обязательное условие:

* Использование системы сборки Gradle
* Код должен быть отлажен и протестирован

# Дедлайн 08.10.2025 23:59
# Дедлайн 08.10.2025 23:59

# Решения

## 1. Упорядочивание блокировок

Решение заключается в том, что последний студент захватывает ложку в другом порядке.
Если все сначала захватывают левую ложку, а затем правую, то последний студент захватывает правую ложку, а затем левую.

Решение расположено в пакете orderedlocks.

Такое решение **не** обеспечивает fairness (все студенты поедят одинаково).

Ниже рассмотрены различные варианты вокруг этого подхода.

### 1.1. Добавление различных задержек

Решение дает fairness в случае, когда работа **вне** критической секции дольше работы внутри критической секции.

Это обосновывается тем, что студенты не конкурируют за захват ресурса.

```java
var config = Config.builder()
.NUMBER_OF_STUDENTS(7)
.NUMBER_OF_SOUP(10_000)
.NUMBER_OF_WAITERS(2)
.TIME_TO_EAT_SOUP_MS(Eat Delay)
.TIME_TO_SPEAK_MS(Speak Delay)
.FAIR_IF_POSSIBLE(false)
.build();
```

| Speak Delay | Eat Delay | Fairness Array |
|-------------|-----------|--------------------------------------------|
| 0 | 0 | [219, 403, 771, 1505, 1778, 5144, 180] |
| 0 | 1 | [574, 858, 1062, 1711, 1862, 3430, 503] |
| 1 | 0 | [1428, 1430, 1428, 1427, 1428, 1430, 1429] |
| 1 | 1 | [1292, 1457, 1481, 1490, 1492, 1496, 1292] |
| 2 | 1 | [1428, 1428, 1429, 1429, 1429, 1429, 1428] |

### 1.2. Использование параметра `fair` в Java API

У классов ReentrantLock и ArrayBlockingQueue есть параметр `fair`, который позволяет гарантировать "честность" при
захвате ресурсов, в моем случае это **почему-то** полностью не решает проблему, но значительно улучшает честность.

При этом стоит помнить, что fairness небесплатная, для демонстрации этого эффекта был реализован JMH
`OrderedLocksTests`, для запуска следует добавить gradlew права на исполнение (`sudo chmod +x ./gradlew`) и вызвать
`./gradlew jmh`. В тесте использовались задержки равные 0. Результат теста будет в `./build/results/jmh/results.txt`.

Результаты на моей машине (MAC M2 MAX 32GB):

```text
Benchmark Mode Cnt Score Error Units
OrderedLocksTests.zeroDelayFairTest avgt 5 171.625 ± 13.561 ms/op
OrderedLocksTests.zeroDelayNonFairTest avgt 5 105.592 ± 27.446 ms/op
```

| fair | fairness array |
|-------|--------------------------------------------|
| false | [227, 433, 724, 1483, 1690, 5291, 152] |
| true | [1179, 1301, 1249, 1333, 1434, 2322, 1182] |

## 2. Использование арбитра

Решение заключается в использовании специального класса-арбитра, который следит за числом одновременно едящих студентов,
это число не должно превышать число студентов - 1. Этот инвариант реализуется через семафор.

Решение находится в пакете semaphore.

Аналогично `ReentrantLock` `Semaphore` имеет параметр `fair`, который вынесен в config.

Это решение показывает более высокую честность, чем решение с упорядочиванием блокировок, даже при `fair = false`.

| fair | fairness array |
|-------|--------------------------------------------|
| false | [1430, 1424, 1446, 1437, 1454, 1436, 1373] |
| true | [1428, 1429, 1428, 1429, 1429, 1430, 1427] |

При этом fairness для семафора тоже не бесплатная, для демонстрации этого эффекта был реализован JMH тест
`SemaphoreLocksTests`, запуск аналогичен решению с упорядочиванием блокировок.

Полные результаты:

```text
Benchmark Mode Cnt Score Error Units
o.l.orderedlocks.OrderedLocksTests.zeroDelayFairTest avgt 5 170.558 ± 26.265 ms/op
o.l.orderedlocks.OrderedLocksTests.zeroDelayNonFairTest avgt 5 106.313 ± 9.687 ms/op
o.l.semaphore.SemaphoreLocksTests.zeroDelayFairTest avgt 5 157.910 ± 62.556 ms/op
o.l.semaphore.SemaphoreLocksTests.zeroDelayNonFairTest avgt 5 117.113 ± 25.536 ms/op
```
7 changes: 7 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id("java")
id("me.champeau.jmh") version "0.7.3"
}

group = "org.labs"
Expand All @@ -10,6 +11,12 @@ 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.30")
annotationProcessor("org.projectlombok:lombok:1.18.30")

testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
52 changes: 52 additions & 0 deletions src/jmh/java/org/labs/orderedlocks/OrderedLocksTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.labs.orderedlocks;

import java.util.concurrent.TimeUnit;
import org.labs.common.Config;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

@Fork(1)
@State(Scope.Benchmark)
@Warmup(iterations = 2)
@Measurement(iterations = 5)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class OrderedLocksTests {

private final Config zeroDelayNonFairTest = Config.builder()
.NUMBER_OF_STUDENTS(7)
.NUMBER_OF_SOUP(10_000)
.NUMBER_OF_WAITERS(2)
.TIME_TO_EAT_SOUP_MS(0)
.TIME_TO_SPEAK_MS(0)
.FAIR_IF_POSSIBLE(false)
.build();

private final Config zeroDelayFairTest = Config.builder()
.NUMBER_OF_STUDENTS(7)
.NUMBER_OF_SOUP(10_000)
.NUMBER_OF_WAITERS(2)
.TIME_TO_EAT_SOUP_MS(0)
.TIME_TO_SPEAK_MS(0)
.FAIR_IF_POSSIBLE(true)
.build();

@Benchmark
public void zeroDelayNonFairTest() throws InterruptedException {
var diningStudents = new DiningStudentsSimulation(zeroDelayNonFairTest);
diningStudents.simulate();
}

@Benchmark
public void zeroDelayFairTest() throws InterruptedException {
var diningStudents = new DiningStudentsSimulation(zeroDelayFairTest);
diningStudents.simulate();
}
}
52 changes: 52 additions & 0 deletions src/jmh/java/org/labs/semaphore/SemaphoreLocksTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.labs.semaphore;

import java.util.concurrent.TimeUnit;
import org.labs.common.Config;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

@Fork(1)
@State(Scope.Benchmark)
@Warmup(iterations = 2)
@Measurement(iterations = 5)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class SemaphoreLocksTests {

private final Config zeroDelayNonFairTest = Config.builder()
.NUMBER_OF_STUDENTS(7)
.NUMBER_OF_SOUP(10_000)
.NUMBER_OF_WAITERS(2)
.TIME_TO_EAT_SOUP_MS(0)
.TIME_TO_SPEAK_MS(0)
.FAIR_IF_POSSIBLE(false)
.build();

private final Config zeroDelayFairTest = Config.builder()
.NUMBER_OF_STUDENTS(7)
.NUMBER_OF_SOUP(10_000)
.NUMBER_OF_WAITERS(2)
.TIME_TO_EAT_SOUP_MS(0)
.TIME_TO_SPEAK_MS(0)
.FAIR_IF_POSSIBLE(true)
.build();

@Benchmark
public void zeroDelayNonFairTest() throws InterruptedException {
var diningStudents = new DiningStudentsSimulation(zeroDelayNonFairTest);
diningStudents.simulate();
}

@Benchmark
public void zeroDelayFairTest() throws InterruptedException {
var diningStudents = new DiningStudentsSimulation(zeroDelayFairTest);
diningStudents.simulate();
}
}
7 changes: 0 additions & 7 deletions src/main/java/org/labs/Main.java

This file was deleted.

18 changes: 18 additions & 0 deletions src/main/java/org/labs/common/Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.labs.common;

import lombok.Builder;
import lombok.RequiredArgsConstructor;

@Builder
@RequiredArgsConstructor
public class Config {

public final int NUMBER_OF_STUDENTS;
public final int NUMBER_OF_SOUP;
public final int NUMBER_OF_WAITERS;

public final long TIME_TO_EAT_SOUP_MS;
public final long TIME_TO_SPEAK_MS;

public final boolean FAIR_IF_POSSIBLE;
}
37 changes: 37 additions & 0 deletions src/main/java/org/labs/common/Kitchen.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.labs.common;

import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Kitchen {

private final AtomicInteger soupCount;

public enum SoupOrderStatus {
OK,
OUT_OF_SOUP,
}

public Kitchen(int initialSoupCount) {
soupCount = new AtomicInteger(initialSoupCount);
}

public SoupOrderStatus getSoup() {
Integer currentSoupCount;

do {
currentSoupCount = soupCount.get();

if (currentSoupCount.equals(0)) {
log.info("Kitchen is out of soup");
return SoupOrderStatus.OUT_OF_SOUP;
}
} while (!soupCount.compareAndSet(currentSoupCount, currentSoupCount - 1));

if (currentSoupCount % 10_000 == 0) {
log.debug("Kitchen soup count {}", currentSoupCount);
}
return SoupOrderStatus.OK;
}
}
28 changes: 28 additions & 0 deletions src/main/java/org/labs/common/Spoon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.labs.common;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import lombok.Getter;

public class Spoon {
@Getter
private final Integer id;
private final Lock lock;

public Spoon(int id, boolean fairness) {
this.id = id;
lock = new ReentrantLock(fairness);
}

public void lock() {
lock.lock();
}

public void unlock() {
lock.unlock();
}

public boolean tryLock() {
return lock.tryLock();
}
}
Loading