← Назад к вопросам
Какая была самая интересная задача с горутинами?
1.6 Junior🔥 71 комментариев
#Soft Skills и карьера
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Какая была самая интересная задача с горутинами
Горутины — это концепция из Go, но в Java мы работаем с потоками и асинхронностью. Расскажу о интересной задаче с многопоточностью.
Задача: Поиск соперника в реальном времени (matching algorithm)
Контекст: Приложение для онлайн-игр. Нужно быстро найти двух игроков примерно одного уровня.
Требования:
- Два игрока спарены за 5-15 секунд
- Race condition prevention
- 100K игроков одновременно
Решение через Spring
@Service
@Slf4j
public class PlayerMatchingService {
private final SortedMap<Integer, Queue<SearchingPlayer>> ratingBuckets;
private final ReentrantReadWriteLock ratingLock;
public PlayerMatchingService() {
this.ratingBuckets = Collections.synchronizedSortedMap(
new TreeMap<>()
);
this.ratingLock = new ReentrantReadWriteLock();
}
public void addPlayerToQueue(SearchingPlayer player) {
ratingLock.writeLock().lock();
try {
int bucket = (player.getRating() / 100) * 100;
ratingBuckets.computeIfAbsent(bucket, k ->
new ConcurrentLinkedQueue<>()
).offer(player);
log.info("Player {} added to rating bucket {}",
player.getPlayerId(), bucket);
} finally {
ratingLock.writeLock().unlock();
}
}
public Optional<GameMatch> findOpponent(SearchingPlayer player) {
ratingLock.readLock().lock();
try {
int targetRating = player.getRating();
for (int offset = 0; offset <= 500; offset += 100) {
if (tryMatchInBucket(targetRating + offset, player)) {
return Optional.of(createMatch(player));
}
if (offset > 0 &&
tryMatchInBucket(targetRating - offset, player)) {
return Optional.of(createMatch(player));
}
}
return Optional.empty();
} finally {
ratingLock.readLock().unlock();
}
}
private boolean tryMatchInBucket(int ratingBucket, SearchingPlayer player) {
Queue<SearchingPlayer> bucket = ratingBuckets.get(ratingBucket);
if (bucket == null || bucket.isEmpty()) {
return false;
}
SearchingPlayer opponent = bucket.poll();
if (opponent != null) {
log.info("Match found: {} vs {}",
player.getPlayerId(), opponent.getPlayerId());
return true;
}
return false;
}
}
WebSocket контроллер
@RestController
@RequestMapping("/api/match")
@Slf4j
public class MatchController {
private final PlayerMatchingService matchingService;
private final SimpMessagingTemplate messagingTemplate;
@PostMapping("/queue")
public ResponseEntity<Void> joinQueue(@RequestBody SearchingPlayer player) {
matchingService.addPlayerToQueue(player);
Executors.newSingleThreadScheduledExecutor().schedule(() -> {
Optional<GameMatch> match = matchingService.findOpponent(player);
if (match.isPresent()) {
messagingTemplate.convertAndSendToUser(
String.valueOf(player.getPlayerId()),
"/queue/match-found",
match.get()
);
}
}, 5, TimeUnit.SECONDS);
return ResponseEntity.ok().build();
}
}
Проблемы и решения
1. Race Condition при поиске
Проблема: Два потока выбирают одного опонента.
Решение: SELECT FOR UPDATE в БД
public Optional<GameMatch> findOpponentWithLock(Long playerId) {
Player player = playerRepository.findByIdForUpdate(playerId);
Optional<Player> opponent = playerRepository
.findFirstInQueue(player.getRating());
if (opponent.isPresent()) {
GameMatch match = new GameMatch(player, opponent.get());
gameMatchRepository.save(match);
return Optional.of(match);
}
return Optional.empty();
}
2. Потокобезопасность очереди
// Плохо
Queue<Player> queue = new LinkedList<>();
queue.add(player);
// Хорошо
Queue<Player> queue = new ConcurrentLinkedQueue<>();
queue.offer(player);
3. Deadlock при обновлении рейтинга
// Плохо
synchronized (player1) {
synchronized (player2) {
player1.addWin();
player2.addLoss();
}
}
// Хорошо — консистентный порядок
private void updateRatings(Player winner, Player loser) {
Player first = winner.getId() < loser.getId() ? winner : loser;
Player second = winner.getId() < loser.getId() ? loser : winner;
synchronized (first) {
synchronized (second) {
winner.addWin();
loser.addLoss();
}
}
}
Оптимизированное решение
@Service
@Slf4j
public class OptimizedMatchingService {
private final NavigableMap<Integer, Queue<SearchingPlayer>> queues;
private final ReentrantReadWriteLock lock;
public OptimizedMatchingService() {
this.queues = new ConcurrentSkipListMap<>();
this.lock = new ReentrantReadWriteLock();
}
public void addPlayer(SearchingPlayer player) {
lock.writeLock().lock();
try {
int bucket = bucketize(player.getRating());
queues.computeIfAbsent(bucket, k ->
new ConcurrentLinkedQueue<>()
).offer(player);
} finally {
lock.writeLock().unlock();
}
}
public Optional<Pair<SearchingPlayer, SearchingPlayer>> findMatch(
SearchingPlayer player) {
lock.readLock().lock();
try {
int rating = player.getRating();
for (int bucket : queues.keySet()) {
if (Math.abs(bucket - rating) <= 300) {
Queue<SearchingPlayer> queue = queues.get(bucket);
SearchingPlayer opponent = queue.peek();
if (opponent != null && opponent.getPlayerId() != player.getPlayerId()) {
queue.poll();
return Optional.of(Pair.of(player, opponent));
}
}
}
return Optional.empty();
} finally {
lock.readLock().unlock();
}
}
private int bucketize(int rating) {
return (rating / 100) * 100;
}
}
Результаты оптимизации
До оптимизации:
- Поиск соперника: 100-200ms
- Пиковая нагрузка: 10K игроков
После оптимизации:
- Поиск соперника: 5-10ms
- Пиковая нагрузка: 100K игроков
Ключевые моменты
- ReadWriteLock — много читают (поиск), редко пишут
- Bucketing — уменьшить диапазон поиска
- ConcurrentCollections — избежать явной синхронизации
- Atomic операции — гарантировать целостность
- WebSocket — реальное обновление UI
Почему это была интересная задача:
- Глубокое понимание многопоточности
- Performance был критичен (100K+)
- Race conditions были трудноловимы
- Оптимизация структуры данных и синхронизации