← Назад к вопросам

Какие знаешь способы решения проблем multithreading?

2.3 Middle🔥 191 комментариев
#Многопоточность и асинхронность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение проблем многопоточности (Multithreading) в Android

Многопоточность в Android вызывает сложные баги, которые трудно воспроизвести. Существует несколько проверенных подходов для решения таких проблем.

Основные проблемы многопоточности

1. Race Conditions (состояние гонки) Два потока одновременно обращаются к одному ресурсу:

// ❌ Проблемный код
var counter = 0

Thread {
    repeat(1000) {
        counter++  // Не атомарная операция
    }
}.start()

Thread {
    repeat(1000) {
        counter++
    }
}.start()

// counter может быть меньше 2000!

2. Deadlock (взаимная блокировка) Два потока ждут друг друга:

// ❌ Deadlock
val lock1 = Any()
val lock2 = Any()

Thread {
    synchronized(lock1) {
        Thread.sleep(100)
        synchronized(lock2) {  // Ждёт lock2
            println("Thread 1")
        }
    }
}.start()

Thread {
    synchronized(lock2) {
        Thread.sleep(100)
        synchronized(lock1) {  // Ждёт lock1
            println("Thread 2")
        }
    }
}.start()

3. Stale Data (старые данные) Поток читает устаревшие значения:

// ❌ Проблема: флаг может не обновиться
var running = true

Thread {
    while (running) {  // Может закэшировать старое значение
        doWork()
    }
}.start()

running = false  // Может не заметить

Способ 1: Synchronization (синхронизация)

synchronized блок

var counter = 0
val lock = Any()

fun increment() {
    synchronized(lock) {
        counter++
    }
}

Synchronized метод

class Counter {
    private var count = 0
    
    @Synchronized
    fun increment() {
        count++
    }
    
    @Synchronized
    fun getCount() = count
}

Способ 2: Atomic классы (без блокировок)

import java.util.concurrent.atomic.AtomicInteger

val counter = AtomicInteger(0)

Thread {
    repeat(1000) {
        counter.incrementAndGet()  // Безопасная операция
    }
}.start()

Thread {
    repeat(1000) {
        counter.incrementAndGet()
    }
}.start()

// Гарантированно 2000

Доступные Atomic классы:

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
  • AtomicReference<T>
val flag = AtomicBoolean(true)

Thread {
    while (flag.get()) {
        doWork()
    }
}.start()

flag.set(false)  // Безопасно видно другому потоку

Способ 3: Volatile переменные

Для простых флагов:

@Volatile
var running = true

Thread {
    while (running) {
        doWork()
    }
}.start()

running = false  // Другой поток сразу заметит

Способ 4: Thread-safe Collections

CopyOnWriteArrayList — для чтения много, писать редко:

import java.util.concurrent.CopyOnWriteArrayList

val list = CopyOnWriteArrayList<String>()

launch {
    repeat(1000) {
        list.add("Item $it")  // Безопасно
    }
}

launch {
    list.forEach { item ->
        println(item)  // Безопасно
    }
}

ConcurrentHashMap — потокобезопасный HashMap:

import java.util.concurrent.ConcurrentHashMap

val map = ConcurrentHashMap<String, User>()

map["user_1"] = user  // Безопасно
val user = map["user_1"]  // Безопасно

Способ 5: Coroutines (современный подход)

Автоматическое управление потоками:

val counter = AtomicInteger(0)

scope.launch {
    repeat(1000) {
        counter.incrementAndGet()
    }
}

scope.launch {
    repeat(1000) {
        counter.incrementAndGet()
    }
}

// Всё работает правильно

С Mutex для синхронизации:

import kotlinx.coroutines.sync.Mutex

val mutex = Mutex()
var counter = 0

scope.launch {
    repeat(1000) {
        mutex.withLock {
            counter++
        }
    }
}

С Actor (каналы):

import kotlinx.coroutines.channels.actor

val countActor = scope.actor<Int> {
    var counter = 0
    for (msg in channel) {
        counter += msg
        println("Counter: $counter")
    }
}

launch { countActor.send(1) }  // Безопасно, очередь
launch { countActor.send(2) }

Способ 6: Immutability (неизменяемость)

Самый безопасный подход — неизменяемые данные:

data class User(
    val id: Int,
    val name: String,
    val email: String
)  // Все поля val — thread-safe

data class AppState(
    val users: List<User>,
    val selectedUser: User?
)  // Неизменяемо

var state = AppState(emptyList(), null)

scope.launch {
    // Вместо изменения, создаём новое состояние
    state = state.copy(users = users)
}

Способ 7: SingleThreadDispatcher

Все операции на одном потоке:

val singleThread = Dispatchers.Main

scope.launch(singleThread) {
    // Все операции на main потоке
    counter++
    updateUI()
}

Таблица сравнения методов

МетодПроизводительностьЛегкостьСлучай использования
SynchronizedНизкаяСредняяРедкие критические секции
AtomicВысокаяВысокаяПростые переменные
VolatileВысокаяВысокаяФлаги
Thread-safe CollectionsСредняяСредняяКоллекции
CoroutinesВысокаяВысокаяГлавный способ в Android
ImmutabilityОчень высокаяНизкаяФункциональное программирование

Best Practices

// 1. Используй Coroutines по умолчанию
launch {
    val result = apiService.fetchData()  // Безопасно
}

// 2. Immutable данные
data class State(val users: List<User>)

// 3. Не используй synchronized если не нужен
// 4. Preferrables Atomic вместо synchronized
val flag = AtomicBoolean(true)

// 5. Логируй и тестируй многопоточность
// 6. Используй ThreadSanitizer для отладки

Отладка проблем

// Логирование потока
Log.d("Thread", Thread.currentThread().name)

// ThreadLocal для thread-specific данных
val threadLocal = ThreadLocal<String>()
threadLocal.set("value")  // Видно только этому потоку

Вывод: для Android Coroutines + Immutable Data — это основной подход. Для других критических операций используй Atomic классы. Избегай synchronized когда возможно.