Как поделиться данными между двумя потоками?
Существует несколько способов передачи данных между потоками. Выбор зависит от паттерна взаимодействия.
1. Общий объект с синхронизацией
Самый простой способ – потоки работают с одним и тем же объектом, доступ синхронизирован:
Пример
public class SharedData {
private String message;
public synchronized void setMessage(String msg) {
this.message = msg;
notify(); // Уведомить ожидающий поток
}
public synchronized String getMessage() throws InterruptedException {
while (message == null) {
wait(); // Ждать, пока данные не будут записаны
}
return message;
}
}
2. BlockingQueue (производитель-потребитель)
Рекомендуемый способ для паттерна «производитель-потребитель»:
Пример: обмен через BlockingQueue
BlockingQueue<String> queue = new LinkedBlockingQueue<>(100);
// Производитель
Thread producer = new Thread(() -> {
try {
queue.put("Сообщение от производителя");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Потребитель
Thread consumer = new Thread(() -> {
try {
String msg = queue.take(); // Блокируется, пока данные не появятся
System.out.println("Получено: " + msg);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
3. Exchanger (обмен между двумя потоками)
Класс Exchanger<V> предназначен для двустороннего обмена данными между ровно двумя потоками. Каждый поток вызывает exchange(), передаёт свои данные и получает данные другого потока:
Пример: обмен через Exchanger
Exchanger<String> exchanger = new Exchanger<>();
Thread t1 = new Thread(() -> {
try {
String fromT2 = exchanger.exchange("Данные от T1");
System.out.println("T1 получил: " + fromT2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread t2 = new Thread(() -> {
try {
String fromT1 = exchanger.exchange("Данные от T2");
System.out.println("T2 получил: " + fromT1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
t1.start();
t2.start();
4. Concurrent-коллекции
Потокобезопасные коллекции из java.util.concurrent:
| Коллекция | Применение |
|---|---|
ConcurrentHashMap |
Конкурентное чтение/запись ключ-значение |
CopyOnWriteArrayList |
Редкая запись, частое чтение |
ConcurrentLinkedQueue |
Неблокирующая очередь |
5. Future / CompletableFuture
Для передачи результата из фонового потока в вызывающий:
Пример
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> "Результат вычисления");
String result = future.get(); // Блокирует до получения результата
6. Pipe (PipedInputStream / PipedOutputStream)
Для потоковой передачи данных (редко используется, есть более удобные альтернативы):
Пример
PipedOutputStream out = new PipedOutputStream();
PipedInputStream in = new PipedInputStream(out);
// Один поток пишет в out, другой читает из in
Сводная таблица
| Способ | Паттерн | Блокировка | Направление |
|---|---|---|---|
Общий объект + synchronized |
Произвольный | Да | Двустороннее |
BlockingQueue |
Производитель-потребитель | Да | Однонаправленное |
Exchanger |
Обмен между парой потоков | Да | Двустороннее |
CompletableFuture |
Асинхронный результат | Опционально | Однонаправленное |
| Concurrent-коллекции | Конкурентный доступ | Частично | Двустороннее |
Аналогия из жизни.
BlockingQueue– это конвейерная лента на фабрике: один рабочий кладёт детали, другой забирает.Exchanger– это рукопожатие с обменом документами: два человека встречаются, каждый отдаёт свой пакет и получает чужой.Future– это квитанция: вы отдаёте задачу и потом приходите за результатом.
На собеседовании. Начните с
BlockingQueue– это самый распространённый и безопасный способ для паттерна «производитель-потребитель». УпомянитеExchanger– его редко знают. Покажите, что знаете разницу между блокирующими и неблокирующими подходами. Также отметьте, что передача данных через общий объект без синхронизации – это race condition.