Java Concurrency
Основы многопоточности Java для Android и существующего Java-кода: Thread, видимость изменений, мониторы, исполнители задач, Future, атомарные значения и конкурентные коллекции. Java-примеры показывают исходные API; те же правила доступа к общему состоянию действуют и в Kotlin.
Потоки и синхронизация
Thread
start() запускает поток, выполняющий run(); прямой вызов run() остаётся обычным вызовом в текущем потоке. Запустить поток можно только один раз. Он завершается после окончания работы или выхода необработанного исключения.
Отмена кооперативная: interrupt() запрашивает реакцию, а не принудительно завершает поток. Код должен проверять прерывание или использовать прерываемые блокирующие вызовы. sleep(), wait() и join() могут выбросить InterruptedException, сбросив статус прерывания. Передайте исключение выше либо восстановите статус через Thread.currentThread().interrupt() и корректно завершите работу; не игнорируйте его молча. Не используйте Thread.stop().
join() ожидает завершения, но не возвращает результат работы. Для результата подходят Callable / Future. Блокирующее ожидание и длительная работа не должны выполняться в главном потоке Android.
volatile vs synchronized
Запись в volatile-поле находится в отношении happens-before к последующим чтениям этого поля. Это также публикует предшествующие записи, но не защищает будущие изменения объекта по ссылке и не делает counter++ атомарным.
class SharedResource {
private volatile boolean flag = false;
public void setFlagTrue() {
flag = true;
}
public boolean isFlag() {
return flag;
}
}
Этому флагу нужна видимость изменений, а не составное обновление. Сам по себе флаг не пробуждает заблокированный поток.
synchronized объединяет взаимное исключение и видимость: освобождение монитора находится в отношении happens-before к последующему захвату того же монитора. Синхронизированные методы экземпляра блокируют this, статические - объект Class класса, в котором объявлены.
class SharedCounter {
private int counter = 0;
public synchronized void increment() {
counter++;
}
public synchronized int getCounter() {
return counter;
}
}
Чтения и записи используют один монитор. Несинхронизированный доступ не становится защищённым из-за наличия другого синхронизированного метода. Делайте критические секции короткими и соблюдайте единый порядок захвата блокировок, чтобы избежать взаимоблокировок. Thread.sleep() не освобождает мониторы и не обеспечивает видимость изменений.
wait() / notify() / notifyAll()
Эти методы Object требуют владения монитором целевого объекта; иначе возникает IllegalMonitorStateException.
wait()освобождает этот монитор, но не остальные блокировки потока. Перед возвратом монитор захватывается снова.notify()выбирает одного ожидающего без гарантии порядка;notifyAll()уведомляет всех ожидающих.- Уведомление не освобождает монитор и не передаёт выполнение ожидающему немедленно.
- Всегда проверяйте условие в цикле
while: ложные пробуждения и конкурирующие потребители могут сделать его неверным. Уведомление не сохраняется для будущих ожидающих.
Читайте и изменяйте состояние условия под одним монитором. Вместо собственного протокола wait/notify предпочитайте BlockingQueue, защёлки или другие высокоуровневые инструменты.
Высокоуровневые API многопоточности
Executor
Executor.execute(Runnable) отделяет задачу от политики выполнения. Он не гарантирует фоновый поток: реализация может выполнить задачу прямо в вызывающем потоке.
ExecutorService добавляет отправку задач, Future и управление жизненным циклом. Переиспользуйте пул с явно определённым владельцем. shutdown() отклоняет новые задачи, но позволяет завершиться принятым; сам вызов не ожидает завершения. shutdownNow() пытается прервать работу и возвращает незапущенные задачи; уже работающие могут продолжить выполнение, если игнорируют прерывание.
Размер пула - не единственное ограничение: фиксированный пул может накапливать неограниченную очередь задач, а кэширующий пул - создавать много потоков. Для постоянной нагрузки осознанно выбирайте размер очереди и поведение при отказе в приёме задач.
Callable / Future
Runnable не возвращает результат; Callable<T> возвращает значение и может выбросить исключение. Future<T>.get() ожидает завершения и возвращает результат либо выбрасывает ExecutionException при ошибке задачи или CancellationException после отмены. Ожидающий поток также может быть прерван.
get(timeout, unit) ограничивает ожидание, а не выполнение задачи; тайм-аут не отменяет её автоматически. cancel(true) может прервать работающую задачу, но не гарантирует остановку её кода. isDone() учитывает ошибку и отмену, а не только успех.
В Kotlin-коде Android корутины с определённой областью жизни и Deferred часто удобнее для результатов и отмены. Они не делают блокирующий Java-код автоматически отменяемым.
Atomic
AtomicInteger, AtomicBoolean и AtomicReference поддерживают атомарные операции над отдельными значениями без ручного захвата монитора. incrementAndGet() атомарен; отдельные вызовы get() и set() не образуют единого атомарного обновления. Не стоит обещать строго lock-free реализацию во всех случаях.
compareAndSet(expected, update) меняет значение, только если оно ещё соответствует ожидаемому; AtomicReference сравнивает ссылки по идентичности. Функции обновления в updateAndGet() могут вызываться повторно, поэтому не должны иметь побочных эффектов.
Несколько атомарных полей не образуют транзакцию. Защищайте связанное состояние одной блокировкой либо атомарно заменяйте неизменяемый объект состояния. Атомарная ссылка не защищает изменения внутри объекта, на который указывает.
ConcurrentHashMap
Предварительные темы
HashMap требует внешней синхронизации при совместном доступе с конкурентными записями. Collections.synchronizedMap(...) сериализует доступ через обёртку; для обхода всё равно нужна блокировка этой обёртки.
ConcurrentHashMap поддерживает конкурентный доступ и атомарные обновления отдельного ключа, но не транзакцию над всей картой:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
ConcurrentMap<String, Integer> counts = new ConcurrentHashMap<>();
counts.put("success", 1);
counts.putIfAbsent("failure", 0);
counts.merge("success", 1, Integer::sum);
Между отдельными вызовами containsKey() и put() возможна гонка. Для единого логического обновления используйте putIfAbsent(), computeIfAbsent(), compute() или merge(). Функции вычисления должны быть короткими; избегайте рекурсивных обновлений карты.
Ключи и значения null запрещены. Итераторы слабо согласованы, а не представляют снимок; агрегатные наблюдения вроде size() могут меняться при конкурентных обновлениях. Потокобезопасность карты не делает её изменяемые значения потокобезопасными.
java.util.concurrent
Полезные инструменты помимо исполнителей и карт:
BlockingQueue: передача данных от производителей потребителям, при необходимости с ограничением ёмкости.CountDownLatch: одноразовое ожидание уменьшения счётчика до нуля.Semaphore: ограничение конкурентного доступа через разрешения.Lock/ReadWriteLock: явные политики блокировки; освобождайте захваченные блокировки вfinally.
В Android выносите блокирующие операции из главного потока и связывайте владение задачами с подходящим жизненным циклом. Перед использованием новых Java API проверяйте уровень Android API или поддержку desugaring.