1. Введение в многопоточность: Main Thread, ошибки ANR и базовые примитивы синхронизации
Введение в многопоточность: Main Thread, ошибки ANR и базовые примитивы синхронизации
Добро пожаловать в курс «Многопоточность в Android: от основ до Kotlin Coroutines». Мы начинаем наше путешествие с фундаментальных понятий, без которых невозможно построить отзывчивое и стабильное приложение. Даже если вы уже используете Coroutines или RxJava, понимание того, что происходит «под капотом» на уровне операционной системы и виртуальной машины Java, критически важно для отладки сложных проблем.
Процессы и потоки: в чем разница?
Прежде чем углубляться в Android, давайте вспомним базовую теорию операционных систем.
Процесс — это экземпляр программы во время выполнения. У него есть свое собственное адресное пространство памяти, которое изолировано от других процессов. Если одно приложение «падает», оно обычно не тянет за собой другие.
Поток (Thread) — это наименьшая единица выполнения внутри процесса. Процесс может содержать один или несколько потоков. Ключевая особенность потоков в том, что они разделяют общую память процесса. Это делает взаимодействие между ними быстрым, но порождает главную проблему многопоточности — конкуренцию за ресурсы.
!Схема процесса, содержащего несколько потоков с общей и локальной памятью
Main Thread: Сердце Android-приложения
Когда вы запускаете Android-приложение, система создает новый процесс Linux и один поток выполнения. Этот поток называется Main Thread (главный поток) или UI Thread (поток пользовательского интерфейса).
Почему он так важен?
В Android (как и в большинстве UI-фреймворков) работа с графическим интерфейсом не является потокобезопасной. Это означает, что все манипуляции с View (нажатия кнопок, отрисовка текста, анимации) должны происходить строго в главном потоке.
Main Thread работает в бесконечном цикле (Event Loop), ожидая событий от пользователя или системы:
onClick) и перерисовал экран.Если Main Thread занят чем-то другим, он не может обновить интерфейс. Приложение перестает реагировать на действия пользователя.
ANR: Application Not Responding
Если вы заблокируете главный поток на длительное время, система покажет диалоговое окно ANR (Application Not Responding). Это худший опыт для пользователя, который часто приводит к удалению приложения.
Когда возникает ANR?
Согласно документации Android, ANR возникает в двух основных случаях:
BroadcastReceiver не завершил выполнение в течение определенного времени (обычно 10-60 секунд, в зависимости от типа).Пример блокировки Main Thread
Представим, что мы решили скачать файл прямо внутри обработчика нажатия кнопки:
Пока метод downloadLargeFile() выполняется, Main Thread заблокирован. Он не может перерисовать кнопку (показать эффект нажатия) и не может обработать следующие касания. Интерфейс «замерзает».
> В современных версиях Android попытка выполнить сетевой запрос в главном потоке вызовет NetworkOnMainThreadException еще до возникновения ANR. Это защитный механизм системы.
Создание потоков в Java
Чтобы избежать ANR, тяжелые операции (сеть, база данных, сложные вычисления) нужно выносить в фоновые потоки. В Java (и Kotlin, так как он работает на JVM) базовым классом для этого является Thread.
Существует два основных способа создания потока:
1. Наследование от класса Thread
2. Реализация интерфейса Runnable (Предпочтительный способ)
Этот способ лучше, так как он отделяет задачу (Runnable) от исполнителя (Thread) и позволяет наследовать класс от чего-то другого.
Важно: Метод start() запускает новый поток, который вызывает метод run(). Если вы вызовете run() напрямую, код выполнится в текущем потоке, и никакой многопоточности не будет.
Проблемы общего состояния (Shared State)
Как только у вас появляется два потока, работающих с одними и теми же данными, возникает риск состояния гонки (Race Condition).
Рассмотрим классический пример счетчика:
Если два потока одновременно вызовут increment(), результат может быть непредсказуемым. Почему? Потому что операция count++ не является атомарной (неделимой). Для процессора это три действия:
count из памяти в регистр.Представим ситуацию с помощью формулы:
Где — конечное значение, — начальное значение, а — шаг инкремента.
Если два потока (А и Б) читают одновременно:
В итоге вместо 2 мы получаем 1. Данные потеряны.
!Иллюстрация Race Condition при инкременте переменной
Базовые примитивы синхронизации
Чтобы решить проблему Race Condition, нам нужно координировать доступ потоков к общим ресурсам. В Java для этого есть встроенные механизмы.
Ключевое слово synchronized
synchronized гарантирует, что в один момент времени только один поток может выполнять указанный блок кода или метод. Это достигается с помощью механизма, называемого монитором (или intrinsic lock).
Теперь, если Поток А вошел в increment(), Поток Б будет ждать (блокироваться), пока А не выйдет из метода.
Ключевое слово volatile
Проблема многопоточности — это не только атомарность, но и видимость (visibility). Современные процессоры имеют кэши (L1, L2, L3). Поток может изменить переменную в своем локальном кэше, но не записать её сразу в основную оперативную память (RAM). Другой поток, работающий на другом ядре, может не увидеть этого изменения.
Ключевое слово volatile говорит компилятору и процессору: «Не кэшируй эту переменную. Всегда читай и пиши её прямо в основную память».
Важно: volatile гарантирует видимость, но НЕ гарантирует атомарность. Для count++ использование volatile не спасет от Race Condition, так как операция инкремента все равно состоит из трех шагов.
Deadlock (Взаимная блокировка)
Использование синхронизации порождает новую опасность — Deadlock.
Это ситуация, когда Поток 1 держит ресурс А и ждет ресурс Б, а Поток 2 держит ресурс Б и ждет ресурс А. Ни один из них не может продолжить работу. Приложение зависает.
!Метафора взаимной блокировки (Deadlock)
Заключение
Мы разобрали фундамент многопоточности: * Main Thread отвечает за UI, его нельзя блокировать. * ANR — результат блокировки Main Thread. * Thread и Runnable — базовые способы запуска кода параллельно. * Race Condition возникает при одновременном доступе к данным. * synchronized и volatile помогают управлять доступом и видимостью данных.
Однако, ручное управление потоками и синхронизацией сложно и чревато ошибками. В следующей статье мы рассмотрим инструменты Android, созданные для упрощения этой работы: Handler, Looper и MessageQueue.