65 KiB
Ответы на экзаменационные вопросы по Java / Kotlin
Все примеры кода взяты из реального учебного проекта — клиент-серверного приложения с GUI на JavaFX.
Часть I: GUI, JavaFX, Локализация
1. Компоненты пользовательского интерфейса. Иерархия компонентов
Что такое компонент?
Компонент (Component) — любой визуальный элемент GUI: кнопка, текстовое поле, таблица, окно. В Java/Kotlin GUI компоненты организованы в иерархию наследования.
Иерархия в AWT/Swing
java.lang.Object
└── java.awt.Component ← базовый визуальный элемент
├── java.awt.Container ← может содержать другие компоненты
│ ├── java.awt.Window
│ │ ├── java.awt.Frame
│ │ └── java.awt.Dialog
│ └── javax.swing.JComponent ← база всех Swing-виджетов
│ ├── JPanel
│ ├── JButton
│ ├── JLabel
│ ├── JTextField
│ ├── JTable
│ └── ... (все J-компоненты)
└── java.awt.Button (AWT)
Иерархия в JavaFX (используется в проекте)
В JavaFX нет AWT/Swing — другое дерево:
javafx.scene.Node ← базовый узел сцены
├── javafx.scene.Parent ← может содержать дочерние узлы
│ ├── javafx.scene.layout.Pane ← менеджер компоновки
│ │ ├── VBox, HBox, GridPane, StackPane, BorderPane...
│ └── javafx.scene.control.Control ← интерактивные элементы
│ ├── Button, Label, TextField, PasswordField
│ ├── ComboBox, CheckBox, ToggleButton
│ ├── TableView, MenuButton, MenuItem
│ └── ToolBar, SplitPane, Separator
└── javafx.scene.canvas.Canvas ← поверхность для рисования
Концепция Stage → Scene → Node
Stage (окно ОС)
└── Scene (контент окна)
└── Node (корневой элемент — обычно Pane)
└── ... (дерево виджетов)
Из проекта: Main.kt
class App : Application() {
override fun start(stage: Stage) {
AuthView(stage) { // Stage = окно ОС
val mainStage = Stage()
MainView(mainStage).show()
}.show()
}
}
Из проекта: MainView.kt — дерево компонентов
private fun buildUI(): BorderPane {
val root = BorderPane() // корневой контейнер
root.top = buildToolbar() // ToolBar сверху
root.center = buildContent() // SplitPane посередине
root.bottom = buildCommandBar() // HBox снизу
return root
}
SplitPane внутри делится на TableView (список объектов) и Canvas (визуализация).
2. Базовые классы Component, Container, JComponent
java.awt.Component
Самый базовый класс AWT. Описывает любой визуальный элемент: имеет позицию (x, y), размер, цвет, шрифт. Ключевые методы:
| Метод | Описание |
|---|---|
setVisible(boolean) |
показать / скрыть |
setEnabled(boolean) |
включить / выключить |
setSize(int, int) |
задать размер |
repaint() |
перерисовать |
addMouseListener(...) |
подписаться на мышь |
java.awt.Container
Расширяет Component. Может содержать другие компоненты (дочерние). Ключевые методы:
| Метод | Описание |
|---|---|
add(Component) |
добавить дочерний компонент |
remove(Component) |
удалить |
setLayout(LayoutManager) |
назначить менеджер компоновки |
getComponents() |
получить массив дочерних |
javax.swing.JComponent
Расширяет Container. База всех Swing-виджетов (кнопок, полей, таблиц). Добавляет:
- поддержку двойной буферизации (нет мерцания)
- механизм бордеров (
setBorder(...)) - поддержку ToolTip (
setToolTipText(...)) - систему рисования через paintComponent(Graphics g)
Пример переопределения рисования в Swing:
class MyPanel extends JPanel {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.RED);
g.fillOval(10, 10, 100, 100);
}
}
JavaFX-аналог в проекте: Canvas
В JavaFX рисование — через Canvas + GraphicsContext:
// CollectionCanvas.kt
class CollectionCanvas : Canvas() {
private val gc: GraphicsContext = graphicsContext2D // аналог Graphics
private fun drawItem(item: HumanBeingProperty, glowIntensity: Double) {
val px = worldToScreenX(item.coordinateX.value.toDouble())
val py = worldToScreenY(item.coordinateY.value.toDouble())
val radius = mapImpactSpeed(item.impactSpeed.value)
val color = ownerColor(item.ownerId.value)
gc.fill = color
gc.fillOval(px - radius, py - radius, radius * 2, radius * 2)
gc.stroke = Color.WHITE
gc.strokeOval(px - radius, py - radius, radius * 2, radius * 2)
}
}
3. Менеджеры компоновки
Менеджер компоновки (Layout Manager) — объект, который автоматически расставляет компоненты внутри контейнера. Без него пришлось бы вручную задавать координаты каждого элемента.
Основные менеджеры в Swing (AWT)
| Менеджер | Поведение |
|---|---|
FlowLayout |
компоненты идут слева направо, как слова в тексте |
BorderLayout |
5 зон: NORTH, SOUTH, EAST, WEST, CENTER |
GridLayout |
сетка одинаковых ячеек N×M |
GridBagLayout |
гибкая сетка с весами и отступами |
BoxLayout |
горизонтальная или вертикальная линия |
CardLayout |
стопка карточек, показывается одна |
null |
абсолютное позиционирование (setBounds) |
Пример Swing:
JPanel panel = new JPanel(new BorderLayout());
panel.add(new JButton("Сверху"), BorderLayout.NORTH);
panel.add(new JTextField(), BorderLayout.CENTER);
panel.add(new JButton("Снизу"), BorderLayout.SOUTH);
Менеджеры компоновки в JavaFX (используются в проекте)
JavaFX встраивает менеджер прямо в класс контейнера:
| Класс | Аналог Swing | Описание |
|---|---|---|
VBox |
BoxLayout вертикальный | дети вертикально |
HBox |
BoxLayout горизонтальный | дети горизонтально |
GridPane |
GridBagLayout | гибкая сетка по строкам/столбцам |
BorderPane |
BorderLayout | 5 зон: top, bottom, left, right, center |
StackPane |
LayeredPane | компоненты стопкой (z-порядок) |
SplitPane |
JSplitPane | делится на части с разделителем |
ToolBar |
JToolBar | панель инструментов |
Из проекта: EditDialog.kt — GridPane для формы
private fun buildUI(): VBox {
val form = GridPane()
form.hgap = 8.0 // горизонтальный отступ между ячейками
form.vgap = 8.0 // вертикальный отступ
form.padding = Insets(16.0)
form.alignment = Pos.CENTER_LEFT
var row = 0
form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.name") }, nameField)
row++
form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.coordinates.x") }, coordXField)
row++
// ...
return VBox(8.0, form, buttonBar) // VBox: форма + кнопки
}
Из проекта: MainView.kt — BorderPane + SplitPane
private fun buildUI(): BorderPane {
val root = BorderPane()
root.top = buildToolbar() // ToolBar: вверху
root.center = buildContent() // SplitPane: по центру
root.bottom = buildCommandBar() // HBox: внизу
return root
}
private fun buildContent(): Node {
val splitPane = SplitPane()
splitPane.orientation = Orientation.HORIZONTAL
splitPane.items.addAll(buildTablePane(), buildCanvasPane())
splitPane.setDividerPositions(0.55) // 55% левой части
return splitPane
}
4. Модель обработки событий. Класс-слушатель и класс-событие
Суть модели событий
В Java GUI используется паттерн Observer (наблюдатель):
- Источник события (компонент) — кнопка, поле и т.д.
- Событие (Event) — объект, описывающий что произошло.
- Слушатель (Listener) — объект с методом-обработчиком, подписанный на источник.
Класс-событие
Каждое событие — объект класса, наследующего java.util.EventObject:
java.util.EventObject
└── java.awt.AWTEvent
├── java.awt.event.ActionEvent (нажатие кнопки)
├── java.awt.event.MouseEvent (мышь)
├── java.awt.event.KeyEvent (клавиатура)
├── java.awt.event.WindowEvent (окно)
└── java.awt.event.ItemEvent (выбор в списке)
Событие содержит: источник (getSource()), тип события, дополнительные данные (координаты мыши, код клавиши и т.д.).
Класс-слушатель (интерфейс Listener)
Каждый тип события — интерфейс с методом-обработчиком:
| Интерфейс | Методы | Событие |
|---|---|---|
ActionListener |
actionPerformed(ActionEvent) |
клик кнопки |
MouseListener |
mouseClicked/Pressed/Released/Entered/Exited |
мышь |
KeyListener |
keyPressed/Released/Typed |
клавиши |
WindowListener |
windowOpened/Closed/... |
окно |
ChangeListener |
stateChanged(ChangeEvent) |
изменение состояния |
Пример в Swing:
JButton btn = new JButton("Нажми");
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Кнопка нажата! Источник: " + e.getSource());
}
});
JavaFX: EventHandler вместо Listener
В JavaFX используется generic-интерфейс EventHandler<T extends Event>:
button.setOnAction { event: ActionEvent ->
println("Нажата кнопка")
}
Иерархия событий JavaFX:
javafx.event.Event
├── javafx.event.ActionEvent (кнопки, меню)
├── javafx.scene.input.MouseEvent
├── javafx.scene.input.KeyEvent
└── javafx.collections.ListChangeListener.Change (коллекции)
Из проекта: обработка кликов мыши в CollectionCanvas.kt
init {
// Добавляем слушатель событий мыши
addEventHandler(MouseEvent.MOUSE_CLICKED, this::onClick)
}
private fun onClick(event: MouseEvent) {
val mx = event.x // координаты клика
val my = event.y
val items = Session.collection.toList()
for (item in items) {
val px = worldToScreenX(item.coordinateX.value.toDouble())
val py = worldToScreenY(item.coordinateY.value.toDouble())
val r = mapImpactSpeed(item.impactSpeed.value)
val dx = mx - px
val dy = my - py
if (dx * dx + dy * dy <= (r + hitRadius) * (r + hitRadius)) {
showItemInfo(item) // показать информацию об объекте
return
}
}
}
Из проекта: слушатель изменения коллекции в MainController.kt
// ListChangeListener — JavaFX-аналог Observer на ObservableList
private val collectionListener = ListChangeListener<HumanBeingProperty> { _ ->
reapplyFiltersAndSort() // пересчитать при любом изменении
}
fun start() {
Session.collection.addListener(collectionListener) // подписка
}
fun stop() {
Session.collection.removeListener(collectionListener) // отписка
}
Из проекта: слушатель изменения текста в MainView.kt
val filterField = TextField()
filterField.textProperty().addListener { _, _, _ ->
// _ = observable, _ = oldValue, _ = newValue
applyFilters()
}
5. Технология JavaFX. Особенности архитектуры, отличия от AWT/Swing
Ключевые отличия
| Характеристика | AWT | Swing | JavaFX |
|---|---|---|---|
| Год появления | 1995 | 1997 | 2008 (JavaFX 2.0 в 2011) |
| Рендеринг | нативные виджеты ОС | Java 2D (программный) | Hardware-ускорение (Prism) |
| UI-описание | Java-код | Java-код | FXML (XML) + Java/Kotlin |
| CSS-стилизация | нет | ограничено | полная поддержка CSS |
| Анимация | нет | Timer | Animation API (Timeline, AnimationTimer) |
| Связывание данных | нет | нет | Property/Binding API |
| Потокобезопасность | EDT (Event Dispatch Thread) | EDT | JavaFX Application Thread |
| 3D-поддержка | нет | нет | да (Scene3D) |
| WebView | нет | нет | да (встроенный браузер) |
Архитектура JavaFX
Application
├── Stage (окно)
│ └── Scene
│ └── Scene Graph (дерево Node)
├── JavaFX Application Thread (поток UI)
└── Prism (рендеринг через GPU: OpenGL / DirectX / Metal)
Scene Graph — это граф объектов (Node). Каждый узел имеет трансформации, эффекты, CSS-класс. Это основное отличие от AWT/Swing.
Property и Binding
JavaFX вводит реактивное связывание: Property<T> — обёртка над значением, которая уведомляет слушателей при изменении.
// HumanBeingProperty.kt — каждое поле объекта — Property
val name = SimpleStringProperty(humanBeing.name)
val realHero = SimpleBooleanProperty(humanBeing.realHero)
val coordinateX = SimpleObjectProperty<Long>(humanBeing.coordinates.x)
Это позволяет привязывать (bind) поля UI прямо к данным:
// TableColumn автоматически отображает значение Property
tableCol.cellValueFactory = Callback { features ->
SimpleObjectProperty(extractor(features.value))
}
JavaFX Application Thread
Весь UI живёт в одном потоке — JavaFX Application Thread. Если нужно обновить UI из фонового потока — используют Platform.runLater { }.
Из проекта: Platform.runLater в AuthController.kt
fun authenticate(..., onResult: (Boolean, String) -> Unit) {
scope.launch { // фоновый поток
val response = provider.auth(...) // сетевой запрос
// Обновление UI — только через Platform.runLater!
Platform.runLater { onResult(true, "") }
}
}
Из проекта: AnimationTimer в CollectionCanvas.kt
private val renderTimer: AnimationTimer = object : AnimationTimer() {
override fun handle(now: Long) {
animationTime = now / 1_000_000_000.0 // время в секундах
render() // перерисовка каждый кадр (~60 FPS)
}
}
AnimationTimer — JavaFX-механизм игрового цикла, вызывающий handle() перед каждым кадром. Аналогов в Swing нет.
Из проекта: ObservableList в Session.kt
object Session {
// ObservableList — специальный список JavaFX:
// уведомляет слушателей при добавлении/удалении элементов
val collection: ObservableList<HumanBeingProperty> = FXCollections.observableArrayList()
}
TableView напрямую привязан к ObservableList — при изменении коллекции таблица обновляется автоматически.
6. Интернационализация. Локализация. Хранение локализованных ресурсов
Интернационализация (i18n) vs Локализация (l10n)
- Интернационализация (i18n) — проектирование программы так, чтобы её можно было адаптировать под разные языки/регионы без изменения кода.
- Локализация (l10n) — конкретная адаптация под конкретный язык/регион (перевод, форматы дат и чисел).
Стандартный Java-подход: ResourceBundle + .properties файлы
Структура файлов:
src/main/resources/
messages.properties ← fallback (английский)
messages_ru.properties ← русский
messages_de.properties ← немецкий
messages_fr_FR.properties ← французский (Франция)
Содержимое messages_ru.properties:
auth.title=Вход / Регистрация
button.add=Добавить
error.not.owner=Вы не владелец этого объекта
Использование в коде:
Locale locale = new Locale("ru");
ResourceBundle bundle = ResourceBundle.getBundle("messages", locale);
String title = bundle.getString("auth.title"); // → "Вход / Регистрация"
Объект Locale
java.util.Locale описывает язык+регион:
Locale ruLocale = new Locale("ru"); // русский
Locale enAuLocale = new Locale("en", "AU"); // английский (Австралия)
Locale.getDefault() // локаль системы
Подход в проекте: Map-based AppLocale.kt
В проекте локализация реализована через HashMap прямо в коде (без .properties файлов). Строки хранятся в Map<Locale, Map<String, String>>:
object AppLocale {
val currentLocale: ObjectProperty<Locale> = SimpleObjectProperty(Locale("ru"))
private val strings: Map<Locale, Map<String, String>> = mapOf(
Locale("ru") to mapOf(
"auth.title" to "Вход / Регистрация",
"button.add" to "Добавить",
"error.not.owner" to "Вы не владелец этого объекта",
// ...
),
Locale("nl") to mapOf(
"auth.title" to "Inloggen / Registreren",
"button.add" to "Toevoegen",
"error.not.owner" to "U bent niet de eigenaar van dit object",
),
Locale("hu") to mapOf(
"auth.title" to "Bejelentkezés / Regisztráció",
// ...
),
Locale("en", "AU") to mapOf(
"auth.title" to "Sign In / Sign Up",
"auth.error.empty" to "Fill in all fields, mate",
// ...
),
)
fun get(key: String): String =
strings[currentLocale.value]?.get(key) ?: key
}
Динамическое переключение языка
Ключевая особенность — реактивная смена языка в реальном времени через Property + bind:
// Привязка текста кнопки к ключу локализации
fun bind(property: Property<String>, key: String) {
property.value = get(key)
currentLocale.addListener { _, _, _ ->
property.value = get(key) // автообновление при смене языка
}
}
fun bindLabel(labeled: Labeled, key: String) {
bind(labeled.textProperty(), key)
}
В MainView.kt при смене локали:
localeCombo.setOnAction {
val selected = localeCombo.selectionModel.selectedItem ?: return@setOnAction
AppLocale.currentLocale.set(selected.locale) // → все bind()-ы обновятся
updateLocale()
}
7. Форматирование локализованных данных. NumberFormat, DateFormat, MessageFormat, ChoiceFormat
java.text.NumberFormat
Форматирует числа в зависимости от локали:
NumberFormat nf = NumberFormat.getInstance(Locale.GERMANY);
nf.format(1234567.89); // → "1.234.567,89" (точка — разделитель тысяч, запятая — дробная)
NumberFormat nf2 = NumberFormat.getInstance(Locale.US);
nf2.format(1234567.89); // → "1,234,567.89"
// Валюта
NumberFormat cf = NumberFormat.getCurrencyInstance(new Locale("ru"));
cf.format(1500.00); // → "1 500,00 ₽"
// Проценты
NumberFormat pf = NumberFormat.getPercentInstance(Locale.US);
pf.format(0.75); // → "75%"
java.text.DateFormat / SimpleDateFormat
Форматирует даты по локали:
DateFormat df = DateFormat.getDateInstance(DateFormat.FULL, new Locale("ru"));
df.format(new Date()); // → "пятница, 12 июня 2026 г."
DateFormat dfEn = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US);
dfEn.format(new Date()); // → "6/12/26"
// SimpleDateFormat — задаём паттерн вручную
SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy HH:mm", new Locale("ru"));
sdf.format(new Date()); // → "12.06.2026 16:00"
Паттерны: d = день, M = месяц, y = год, H = час (24ч), m = минута, s = секунда.
java.text.MessageFormat
Форматирует строки с параметрами (шаблоны с {0}, {1}):
String template = "Пользователь {0} добавил {1} объект(ов)";
MessageFormat mf = new MessageFormat(template, new Locale("ru"));
String result = mf.format(new Object[]{"Alice", 3});
// → "Пользователь Alice добавил 3 объект(ов)"
Можно задавать форматирование прямо в шаблоне:
// {1,number,#.##} — число с 2 знаками после запятой
String tpl = "Скорость: {0,number,#.##} м/с";
MessageFormat.format(tpl, 123.456789); // → "Скорость: 123.46 м/с"
// {1,date,short} — короткая дата
String tpl2 = "Создан: {0,date,short}";
MessageFormat.format(tpl2, new Date()); // → "Создан: 12.06.26"
java.text.ChoiceFormat
Выбирает строку в зависимости от числового значения (для правильного склонения):
// Паттерн: "0#нет файлов|1#один файл|2#два файла|5#пять файлов"
// Числа задают границы диапазонов
double[] limits = {0, 1, 2, 5};
String[] formats = {"нет файлов", "один файл", "два файла", "много файлов"};
ChoiceFormat cf = new ChoiceFormat(limits, formats);
cf.format(0); // → "нет файлов"
cf.format(1); // → "один файл"
cf.format(3); // → "два файла"
cf.format(10); // → "много файлов"
ChoiceFormat часто используют внутри MessageFormat:
String pattern = "У вас {0,choice,0#нет сообщений|1#одно сообщение|2<{0} сообщения}";
MessageFormat.format(pattern, 2); // → "У вас 2 сообщения"
В проекте: хранение локализованных дат
В HumanBeingProperty.kt поле даты — LocalDate. При отображении в таблице оно конвертируется в строку через toString(). В продакшн-коде лучше использовать DateTimeFormatter:
val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(AppLocale.currentLocale.value)
creationDate.value.format(formatter) // → "12.06.26" для ru, "6/12/26" для en_US
Часть II: Многопоточность и JDBC
1. Многопоточность. Класс Thread, интерфейс Runnable, модификатор synchronized
Поток (Thread)
Поток — независимый поток выполнения кода в рамках одного JVM-процесса. Все потоки делят общую кучу (heap), но имеют свой стек.
Создание потока: два способа
Способ 1: наследование от Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Выполняется в потоке: " + Thread.currentThread().getName());
}
}
MyThread t = new MyThread();
t.start(); // запуск (не run()!)
Способ 2: реализация Runnable (предпочтительно)
Runnable task = () -> System.out.println("Работаю в потоке");
Thread t = new Thread(task);
t.start();
Runnable — функциональный интерфейс с одним методом void run(). Предпочтителен, т.к. не ограничивает наследование.
Жизненный цикл потока
NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED
Модификатор synchronized
Проблема: при одновременном доступе нескольких потоков к общим данным возникают состояния гонки (race conditions).
synchronized гарантирует, что только один поток в момент времени выполняет защищённый блок. Работает через монитор объекта (встроенный мьютекс).
// 1. Синхронизированный метод (монитор = this)
public synchronized void increment() {
counter++;
}
// 2. Синхронизированный блок (явное указание монитора)
public void increment() {
synchronized (this) {
counter++;
}
}
// 3. Статический synchronized (монитор = Class объект)
public static synchronized void staticMethod() { ... }
Из проекта: CollectionManager.kt — синхронизация коллекции
class CollectionManager {
// Collections.synchronizedSortedSet — потокобезопасная обёртка
private val collection: MutableSet<HumanBeing> =
Collections.synchronizedSortedSet(TreeSet())
fun update(id: UUID, updated: HumanBeing): Boolean {
synchronized(collection) { // явный synchronized-блок
val old = getById(id) ?: return false
collection.remove(old)
val replaced = updated.copy(id = old.id, creationDate = old.creationDate)
return collection.add(replaced)
}
}
fun getAll(): TreeSet<HumanBeing> {
synchronized(collection) {
return TreeSet(collection) // снимок коллекции
}
}
}
Из проекта: CacheInvalidator.kt — фоновый поток
class CacheInvalidator(
private val dataSource: DataSource,
private val onInvalidated: () -> Unit
) {
@Volatile
private var running = false
private var thread: Thread? = null
fun start() {
running = true
thread = Thread { // создание потока через Runnable (лямбда)
while (running) {
// слушаем PostgreSQL NOTIFY
Thread.sleep(500) // пауза 500мс между проверками
}
}.also { it.isDaemon = true } // демон-поток: не мешает остановке JVM
.apply {
name = "pg-notify-listener"
start() // запуск
}
}
fun stop() {
running = false
thread?.interrupt() // прерываем сон через interrupt()
}
}
2. Методы wait(), notify(), Lock и Condition
wait() и notify() из класса Object
Эти методы работают только внутри synchronized-блока. Они реализуют схему "ожидание условия":
wait()— текущий поток освобождает монитор и засыпает, пока другой поток не вызоветnotify().notify()— будит один ожидающий поток.notifyAll()— будит все ожидающие потоки.
Классический пример: очередь производитель-потребитель
class Buffer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 10;
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // ждём, пока не освободится место
}
queue.add(item);
notify(); // будим потребителя
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // ждём, пока не появится элемент
}
int item = queue.poll();
notify(); // будим производителя
return item;
}
}
Интерфейс Lock (java.util.concurrent.locks)
Lock — более гибкая альтернатива synchronized. Основная реализация — ReentrantLock.
| Возможность | synchronized | ReentrantLock |
|---|---|---|
| Явное получение/освобождение | нет | lock() / unlock() |
| Попытка без блокировки | нет | tryLock() |
| Попытка с таймаутом | нет | tryLock(time, unit) |
| Прерываемое ожидание | нет | lockInterruptibly() |
| Справедливость (FIFO) | нет | new ReentrantLock(true) |
Lock lock = new ReentrantLock();
try {
lock.lock(); // захват монитора
// критическая секция
} finally {
lock.unlock(); // ВСЕГДА в finally!
}
// tryLock — не блокируется
if (lock.tryLock()) {
try { /* работа */ } finally { lock.unlock(); }
} else {
System.out.println("Ресурс занят");
}
Интерфейс Condition
Condition — аналог wait()/notify() для Lock. Создаётся из объекта Lock:
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// Производитель:
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // ждём "не полон"
}
queue.add(item);
notEmpty.signal(); // сигналим "не пуст"
} finally { lock.unlock(); }
// Потребитель:
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // ждём "не пуст"
}
int item = queue.poll();
notFull.signal(); // сигналим "не полон"
} finally { lock.unlock(); }
3. Классы-синхронизаторы из пакета java.util.concurrent
java.util.concurrent предоставляет готовые синхронизаторы для типовых сценариев.
Основные синхронизаторы
| Класс | Назначение |
|---|---|
Semaphore |
ограничение числа одновременных доступов |
CountDownLatch |
ожидание выполнения N задач |
CyclicBarrier |
синхронизация N потоков в точке |
Exchanger |
обмен объектами между двумя потоками |
Phaser |
гибкий многофазный барьер |
Semaphore — контролируем число одновременных доступов
// Не более 3 потоков одновременно могут работать с ресурсом
Semaphore semaphore = new Semaphore(3);
void accessResource() throws InterruptedException {
semaphore.acquire(); // захватить "разрешение"
try {
// работа с ограниченным ресурсом
} finally {
semaphore.release(); // вернуть "разрешение"
}
}
CountDownLatch — ждём завершения N задач
CountDownLatch latch = new CountDownLatch(3); // ждём 3 задачи
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// выполняем задачу
latch.countDown(); // уменьшаем счётчик
}).start();
}
latch.await(); // главный поток ждёт, пока счётчик не = 0
System.out.println("Все 3 задачи выполнены!");
CyclicBarrier — все потоки ждут друг друга в точке
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("Все потоки достигли барьера!");
});
Runnable task = () -> {
// часть работы...
barrier.await(); // ждём остальных
// продолжаем...
};
В проекте: готовые пулы потоков как синхронизированное решение
В NetworkManager.kt сервера используются пулы потоков вместо ручной синхронизации:
private val readerPool = Executors.newFixedThreadPool(4) // 4 потока-читателя
private val processingPool = ForkJoinPool( // пул для обработки
Runtime.getRuntime().availableProcessors()
)
private val senderPool = ForkJoinPool( // пул для отправки
Runtime.getRuntime().availableProcessors()
)
4. Модификатор volatile. Атомарные типы данных и операции
Проблема видимости (visibility)
Каждый поток может кешировать значения переменных в регистрах CPU или L1/L2 кеше. Изменение переменной одним потоком может быть невидимо другим.
volatile
volatile гарантирует:
- Видимость: запись всегда идёт в main memory, чтение — всегда из main memory (без кеша).
- Запрет переупорядочивания (happens-before на запись/чтение).
volatile boolean running = true; // без volatile второй поток может не увидеть false
// Поток 1:
running = false;
// Поток 2:
while (running) {
// без volatile этот цикл может никогда не закончиться!
}
Ограничение: volatile НЕ делает операцию i++ атомарной, т.к. это три операции: read → increment → write.
Из проекта: @Volatile в CacheInvalidator.kt
@Volatile // Kotlin-аналог volatile
private var running = false
fun start() {
running = true
thread = Thread {
while (running) { // читаем актуальное значение каждый раз
try {
// ...
Thread.sleep(500)
} catch (e: Exception) {
if (!running) break // проверяем после пробуждения
}
}
}
}
fun stop() {
running = false // запись видна другому потоку сразу
thread?.interrupt()
}
Атомарные типы: java.util.concurrent.atomic
Атомарные типы позволяют делать составные операции (read-modify-write) атомарно без synchronized. Реализованы через CPU-инструкции CAS (Compare-And-Swap).
| Класс | Описание |
|---|---|
AtomicInteger |
атомарный int |
AtomicLong |
атомарный long |
AtomicBoolean |
атомарный boolean |
AtomicReference<V> |
атомарная ссылка |
AtomicIntegerArray |
атомарный массив int |
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // атомарный i++, возвращает новое
counter.getAndIncrement(); // атомарный i++, возвращает старое
counter.addAndGet(5); // атомарный i += 5
counter.compareAndSet(5, 10); // если == 5, то установить 10 (CAS)
Из проекта: AtomicInteger в GatewayManager.kt
// Атомарный счётчик для round-robin балансировки нагрузки
private val roundRobinCounter = AtomicInteger(0)
private fun pickBackend(): BackendServer? {
val alive = backends.filter { it.isAlive }
if (alive.isEmpty()) return null
// getAndIncrement() — атомарно: read + increment + write
return alive[roundRobinCounter.getAndIncrement() % alive.size]
}
5. Коллекции из пакета java.util.concurrent
Обычные коллекции (ArrayList, HashMap, TreeSet) не потокобезопасны. Collections.synchronizedXxx() делает их потокобезопасными, но с глобальной блокировкой (плохая производительность).
java.util.concurrent предоставляет высокопроизводительные потокобезопасные коллекции.
Основные коллекции
| Класс | Описание |
|---|---|
ConcurrentHashMap<K,V> |
HashMap с сегментной блокировкой (высокая пропускная способность) |
CopyOnWriteArrayList<T> |
список: запись = копирование, чтение без блокировки |
CopyOnWriteArraySet<T> |
то же, но Set |
ConcurrentLinkedQueue<T> |
неблокирующая очередь (CAS) |
ConcurrentLinkedDeque<T> |
неблокирующая двусторонняя очередь |
LinkedBlockingQueue<T> |
блокирующая очередь (производитель-потребитель) |
PriorityBlockingQueue<T> |
блокирующая очередь с приоритетом |
ArrayBlockingQueue<T> |
ограниченная блокирующая очередь |
ConcurrentHashMap
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.putIfAbsent("key", 2); // атомарная операция
map.computeIfAbsent("key", k -> 42); // атомарное вычисление
CopyOnWriteArrayList
При каждой записи создаётся копия массива. Читатели не блокируются. Оптимален для редких записей и частых чтений.
List<String> list = new CopyOnWriteArrayList<>();
list.add("a"); // копирует массив, добавляет, заменяет ссылку
// Итерация безопасна даже при параллельных записях:
for (String s : list) { /* ... */ }
LinkedBlockingQueue — производитель-потребитель
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
// Производитель:
queue.put(task); // блокируется если очередь полна
// Потребитель:
Task t = queue.take(); // блокируется если очередь пуста
Из проекта: CopyOnWriteArrayList в GatewayManager.kt
// CopyOnWriteArrayList: список серверов читается часто (при каждом запросе),
// пишется редко (при обнаружении/удалении серверов)
private val backends = CopyOnWriteArrayList<BackendServer>()
// Безопасное чтение без блокировки:
private fun pickBackend(): BackendServer? {
val alive = backends.filter { it.isAlive } // итерация без блокировки
// ...
}
// Запись (создаёт копию):
backends.addAllAbsent(newBackends)
backends.removeAll { ... }
6. Интерфейсы Executor, ExecutorService, Callable, Future
Проблема прямого создания потоков
Создавать new Thread() для каждой задачи — дорого. Лучше переиспользовать потоки через пулы.
Executor
Самый простой интерфейс: принимает Runnable на выполнение:
interface Executor {
void execute(Runnable command);
}
ExecutorService
Расширяет Executor. Добавляет управление жизненным циклом и возможность получать результаты:
ExecutorService es = Executors.newFixedThreadPool(4);
// submit Runnable (нет результата):
es.submit(() -> System.out.println("задача"));
// submit Callable (с результатом):
Future<Integer> future = es.submit(() -> 2 + 2);
int result = future.get(); // блокирует до получения результата
// Завершение:
es.shutdown(); // ждёт завершения задач
es.shutdownNow(); // прерывает запущенные задачи
Callable<V>
Как Runnable, но возвращает результат и может бросить checked-исключения:
interface Callable<V> {
V call() throws Exception;
}
Callable<String> task = () -> {
// долгая операция
return "результат";
};
Future<V>
Представляет будущий результат асинхронной задачи:
Future<String> future = executorService.submit(task);
future.isDone(); // завершена ли задача?
future.isCancelled(); // отменена?
future.cancel(true); // попытка отмены (true = прервать поток)
future.get(); // блокирует до готовности
future.get(5, TimeUnit.SECONDS); // с таймаутом
Фабричные методы Executors
Executors.newFixedThreadPool(4) // пул из 4 потоков
Executors.newCachedThreadPool() // динамический пул (рождаются по требованию)
Executors.newSingleThreadExecutor() // один поток, очередь задач
Executors.newScheduledThreadPool(2) // для отложенных/периодических задач
Из проекта: ExecutorService в NetworkManager.kt
// Сервер использует несколько пулов для изоляции задач:
private val readerPool = Executors.newFixedThreadPool(4) // читают сокет
private val processingPool = ForkJoinPool(...) // обрабатывают запросы
private val senderPool = ForkJoinPool(...) // отправляют ответы
fun start() {
repeat(4) {
readerPool.submit(::readLoop) // запуск 4 задач чтения
}
}
fun stop() {
socket.close()
readerPool.shutdownNow() // прерываем чтение
processingPool.shutdown() // ждём обработки
senderPool.shutdown() // ждём отправки
}
Из проекта: ScheduledExecutorService в GatewayManager.kt
private val healthPool = Executors.newScheduledThreadPool(2)
private fun startHealthChecks() {
// Запускать проверку каждые 5 секунд
healthPool.scheduleAtFixedRate(
{ backends.forEach { probeServer(it) } },
healthCheckIntervalMs, // начальная задержка
healthCheckIntervalMs, // интервал
TimeUnit.MILLISECONDS
)
}
7. Пулы потоков
Пул потоков — набор заранее созданных потоков, которые переиспользуются для выполнения задач.
Зачем нужны пулы
- Создание потока — дорогая операция (~1мс, ~1МБ памяти на стек).
- Бесконтрольное создание потоков перегрузит систему.
- Пул: потоки создаются один раз, задачи ставятся в очередь.
Типы пулов
Метод Executors |
Поведение |
|---|---|
newFixedThreadPool(n) |
ровно n потоков; очередь задач не ограничена |
newCachedThreadPool() |
потоки создаются при необходимости, удаляются через 60с простоя |
newSingleThreadExecutor() |
1 поток, задачи выполняются последовательно |
newScheduledThreadPool(n) |
n потоков, поддержка задержек и периодических задач |
ForkJoinPool(n) |
work-stealing: поток может "украсть" задачу у другого |
ForkJoinPool и рекурсивные задачи
Оптимален для рекурсивных задач (divide & conquer). Использует work-stealing: незанятый поток берёт задачу из очереди другого потока.
// RecursiveTask — возвращает результат
class SumTask extends RecursiveTask<Long> {
@Override
protected Long compute() {
if (размер_маленький) {
return посчитать_напрямую();
}
SumTask left = new SumTask(левая_половина);
SumTask right = new SumTask(правая_половина);
left.fork(); // запустить левую асинхронно
return right.compute() + left.join(); // правую здесь + ждать левую
}
}
ForkJoinPool pool = new ForkJoinPool();
long result = pool.invoke(new SumTask(data));
Из проекта: ForkJoinPool для обработки запросов (NetworkManager.kt)
// RecursiveTask для обработки запроса
inner class ProcessRequestTask(
private val json: String,
private val address: InetAddress,
private val clientPort: Int
) : RecursiveTask<Unit>() {
override fun compute() {
val response = invoker.execute(request) // выполнить команду
senderPool.submit(SendResponseTask(response, address, clientPort)) // отправить
}
}
// RecursiveAction для отправки (нет возвращаемого значения)
inner class SendResponseTask(...) : RecursiveAction() {
override fun compute() {
val bytes = AppJson.encodeToString(...).toByteArray()
val packet = DatagramPacket(bytes, bytes.size, address, clientPort)
synchronized(socket) { socket.send(packet) }
}
}
Аналогично устроен GatewayManager.kt с ProxyTask и SendTask.
8. JDBC. Порядок взаимодействия с базой данных. DriverManager. Connection
Что такое JDBC
JDBC (Java Database Connectivity) — стандартный API для работы с реляционными БД из Java. Предоставляет единый интерфейс независимо от типа БД (PostgreSQL, MySQL, SQLite...).
Порядок взаимодействия с БД
1. Загрузить драйвер
2. Получить соединение (Connection) через DriverManager
3. Создать Statement или PreparedStatement
4. Выполнить запрос
5. Обработать ResultSet
6. Закрыть ресурсы (ResultSet → Statement → Connection)
DriverManager — получение соединения
// Строка подключения: jdbc:<субпротокол>://<хост>:<порт>/<база>
String url = "jdbc:postgresql://localhost:5432/mydb";
String user = "postgres";
String pass = "secret";
Connection conn = DriverManager.getConnection(url, user, pass);
DriverManager автоматически находит нужный драйвер (если он в classpath). При подключении к PostgreSQL нужен артефакт org.postgresql:postgresql.
Connection — соединение с БД
java.sql.Connection — главный объект, представляющий открытое соединение.
// Управление транзакциями:
conn.setAutoCommit(false); // отключить авто-коммит
// ... выполнить запросы ...
conn.commit(); // подтвердить транзакцию
conn.rollback(); // откатить при ошибке
// Закрытие:
conn.close(); // ОБЯЗАТЕЛЬНО закрывать!
Лучшая практика — try-with-resources:
try (Connection conn = DriverManager.getConnection(url, user, pass)) {
// работа с conn
} // автоматически закрывается
Connection Pool: HikariCP в проекте
В проекте используется HikariCP — пул соединений. Соединения дорого создавать, поэтому их переиспользуют:
// DatabaseManager.kt
fun start() {
val config = HikariConfig().apply {
jdbcUrl = System.getenv("DB_URL") ?: "jdbc:postgresql://localhost:5432/prog7_db"
driverClassName = "org.postgresql.Driver"
username = System.getenv("DB_USER") ?: error("DB_USER not set")
password = System.getenv("DB_PASS") ?: error("DB_PASS not set")
maximumPoolSize = 11 // максимум 11 соединений в пуле
isAutoCommit = false
transactionIsolation = "TRANSACTION_REPEATABLE_READ"
}
dataSource = HikariDataSource(config)
Database.connect(dataSource) // Exposed ORM использует этот пул
}
9. Интерфейсы Statement, PreparedStatement, ResultSet, RowSet
Statement — простой запрос
Выполняет SQL-запрос без параметров. Уязвим к SQL-инъекциям — никогда не используйте для запросов с пользовательским вводом!
Statement stmt = conn.createStatement();
// DML (изменение данных):
int affected = stmt.executeUpdate("DELETE FROM users WHERE id = 1");
// DQL (выборка):
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
// DDL (CREATE, DROP, ALTER):
stmt.execute("CREATE TABLE test (id INT)");
stmt.close();
PreparedStatement — параметризованный запрос
Компилируется один раз, выполняется много раз с разными параметрами. Защищает от SQL-инъекций.
PreparedStatement pstmt = conn.prepareStatement(
"INSERT INTO users (username, password) VALUES (?, ?)"
);
pstmt.setString(1, "alice"); // параметр 1
pstmt.setString(2, "hashed"); // параметр 2
pstmt.executeUpdate(); // выполнить
// Переиспользование:
pstmt.setString(1, "bob");
pstmt.setString(2, "hash2");
pstmt.executeUpdate();
pstmt.close();
Batch-выполнение:
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO t VALUES (?)");
for (int i = 0; i < 1000; i++) {
pstmt.setInt(1, i);
pstmt.addBatch(); // добавить в пакет
}
pstmt.executeBatch(); // выполнить весь пакет одним запросом к БД
ResultSet — результат запроса
Курсор для итерации по строкам результата:
PreparedStatement pstmt = conn.prepareStatement(
"SELECT id, name, speed FROM humanBeing WHERE owner_id = ?"
);
pstmt.setString(1, ownerId.toString());
ResultSet rs = pstmt.executeQuery();
while (rs.next()) { // перейти к следующей строке
UUID id = UUID.fromString(rs.getString("id"));
String name = rs.getString("name");
double speed = rs.getDouble("speed");
System.out.println(id + ": " + name + " speed=" + speed);
}
rs.close();
Методы получения значений: getString(col), getInt(col), getDouble(col), getBoolean(col), getDate(col), getObject(col).
RowSet — автономный ResultSet
RowSet — расширение ResultSet, которое:
- может существовать без открытого соединения (
CachedRowSet) - поддерживает сериализацию и передачу по сети
- сам управляет соединением
// CachedRowSet — загружает данные в память, соединение закрывается
CachedRowSet crs = RowSetProvider.newFactory().createCachedRowSet();
crs.setUrl("jdbc:postgresql://localhost:5432/db");
crs.setUsername("user");
crs.setPassword("pass");
crs.setCommand("SELECT * FROM users");
crs.execute(); // загрузить данные (соединение закроется)
while (crs.next()) {
System.out.println(crs.getString("username"));
}
// Соединение к БД уже закрыто, данные в памяти
В проекте: Exposed ORM поверх JDBC (Repository.kt)
Проект использует Exposed (Kotlin ORM) как обёртку над JDBC. Exposed генерирует PreparedStatement автоматически:
// Repository.kt — под капотом это PreparedStatement
fun login(username: String, password: String): Pair<String, String>? {
var id: String? = null
transaction {
// Exposed генерирует: SELECT id FROM "user" WHERE username=? AND password=?
id = UserTable.select(UserTable.id)
.where { (UserTable.username eq username) and (UserTable.password eq password) }
.singleOrNull()
?.get(UserTable.id)?.toString()
}
// ...
}
fun write(item: HumanBeing): UUID? {
transaction {
// INSERT INTO coordinates (x, y) VALUES (?, ?)
val cords = CoordinatesTable.insertAndGetId {
it[x] = item.coordinates.x
it[y] = item.coordinates.y
}
// INSERT INTO humanBeing (...) VALUES (...)
humanId = HumanBeingTable.insertAndGetId {
it[owner_id] = item.ownerId
it[name] = item.name
it[coordinates] = cords
// ...
}.value
exec("NOTIFY cache_changed") // PostgreSQL NOTIFY
}
}
10. Шаблоны проектирования
Шаблон проектирования (Design Pattern) — проверенное решение типовой проблемы в проектировании ПО. Делятся на три группы:
- Порождающие (Creational) — создание объектов
- Структурные (Structural) — организация классов
- Поведенческие (Behavioral) — взаимодействие объектов
Команда (Command) — поведенческий
Проблема: нужно инкапсулировать запрос как объект, поддерживать отмену операций, логировать команды.
Решение: каждое действие — отдельный класс, реализующий интерфейс Command.
// Интерфейс команды (Command.kt)
interface Command {
val name: String
val description: String
val args: List<CommandArgType>
val type: CommandType
fun execute(args: List<String>, humanBeing: HumanBeing? = null, userId: String = ""): Response
}
// Конкретная команда (AddCommand.kt)
class AddCommand(
private val manager: CollectionManager,
private val repository: Repository
) : Command {
override val name = "add"
override val description = "Добавить объект в коллекцию"
override val type = CommandType.MODEL
override val args = listOf(CommandArgType.NONE)
override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response {
val id = repository.write(humanBeing!!) ?: return Response(false, "Ошибка БД")
val saved = humanBeing.copy(id = id)
manager.add(saved)
return Response(true, "Объект добавлен: $id")
}
}
// Инвокер (CommandInvoker.kt) — хранит и запускает команды
class CommandInvoker(private val jwtManager: JWTManager) {
private val commands = mutableMapOf<String, Command>()
private val history = ArrayDeque<String>()
fun register(command: Command) {
commands[command.name] = command
}
fun execute(request: Request): Response {
val command = commands[request.commandName]
?: return Response(false, "Неизвестная команда")
addToHistory(request.commandName)
return command.execute(request.args, request.humanBeing, userId)
}
}
Сервер регистрирует все команды в Main.kt:
fun registerServerCommands(invoker: CommandInvoker, manager: CollectionManager, repository: Repository) {
listOf(
InfoCommand(manager),
ShowCommand(manager),
AddCommand(manager, repository),
UpdateCommand(manager, repository),
RemoveByIdCommand(manager, repository),
// ...
).forEach { invoker.register(it) }
}
Observer (Наблюдатель) — поведенческий
Проблема: объект должен уведомлять другие объекты об изменениях, не зная, кто именно слушает.
В JavaFX это реализовано через ObservableList и ListChangeListener:
// Session.kt — Observable (субъект)
val collection: ObservableList<HumanBeingProperty> = FXCollections.observableArrayList()
// CollectionCanvas.kt — Observer (наблюдатель)
Session.collection.addListener { _: ListChangeListener.Change<out HumanBeingProperty>? ->
scheduleRender() // перерисовать при любом изменении коллекции
}
// MainController.kt — ещё один Observer
private val collectionListener = ListChangeListener<HumanBeingProperty> { _ ->
reapplyFiltersAndSort()
}
fun start() {
Session.collection.addListener(collectionListener)
}
Singleton — порождающий
Проблема: объект должен существовать в единственном экземпляре.
В проекте это Session и AppLocale через Kotlin-объекты (object):
// Session.kt — единственный экземпляр сессии
object Session {
var client: ClientProvider? = null
var username: String = ""
val collection: ObservableList<HumanBeingProperty> = FXCollections.observableArrayList()
}
// AppLocale.kt — единственный менеджер локализации
object AppLocale {
val currentLocale: ObjectProperty<Locale> = SimpleObjectProperty(Locale("ru"))
fun get(key: String): String = strings[currentLocale.value]?.get(key) ?: key
}
MVC (Model-View-Controller) — архитектурный
Проект использует паттерн MVC:
| Слой | Классы в проекте | Описание |
|---|---|---|
| Model | HumanBeing, Session, CollectionManager |
данные и бизнес-логика |
| View | MainView, AuthView, EditDialog, CollectionCanvas |
UI, отображение |
| Controller | MainController, AuthController |
связь View и Model |
// View (MainView.kt) создаёт Controller и вызывает его методы:
class MainView(private val stage: Stage) {
private val controller = MainController()
private fun onAdd() {
val dialog = EditDialog(stage.scene.window)
val humanBeing = dialog.showAndWait() ?: return
controller.executeCommand(name = "add", humanBeing = humanBeing) { success, message ->
if (success) controller.requestRefresh() else showError(message)
}
}
}
// Controller (MainController.kt) координирует логику:
class MainController {
fun executeCommand(name: String, ..., callback: (Boolean, String) -> Unit) {
scope.launch { // фоновый поток
val response = Session.client?.execute(name, args, humanBeing)
Platform.runLater { callback(success, message) } // обновление View
}
}
}
Strategy (Стратегия) — поведенческий
Проблема: нужно иметь несколько алгоритмов и переключаться между ними в runtime.
В проекте реализована через Comparator при сортировке:
// MainView.kt — динамическое создание стратегии сортировки
private fun toggleSort(key: String) {
val comparator: Comparator<HumanBeingProperty> = Comparator { a, b ->
val va = extractSortValue(a, key)
val vb = extractSortValue(b, key)
if (sortAscending) va.compareTo(vb) else vb.compareTo(va)
}
controller.updateComparators(listOf(comparator))
}
// StreamUtils.kt — применение стратегии
fun applyFiltersAndSort(..., comparators: List<Comparator<HumanBeingProperty>>): ObservableList<...> {
if (comparators.isNotEmpty()) {
val combined = comparators.reduce { a, b -> a.thenComparing(b) }
stream = stream.sorted(combined) // применяем выбранную стратегию
}
}
Proxy — структурный
Проблема: нужен объект-посредник для контроля доступа к другому объекту.
В проекте — GatewayManager выступает прокси-сервером:
// GatewayManager.kt — принимает запросы клиентов и перенаправляет на backend
inner class ProxyTask(...) : RecursiveTask<Unit>() {
override fun compute() {
val backend = pickBackend() // выбрать живой сервер (round-robin)
// Прозрачно пересылаем запрос на backend:
DatagramSocket().use { bs ->
bs.send(DatagramPacket(data, data.size, InetAddress.getByName(backend.host), backend.port))
bs.receive(respPacket)
senderPool.submit(SendTask(respPacket.data.copyOf(respPacket.length), clientAddress, clientPort))
}
}
}