Что такое гонка данных (Data Race) в многопоточности?

Ответ

Гонка данных (Data Race) — это ошибка многопоточного программирования, возникающая, когда два или более потока одновременно обращаются к одной и той же разделяемой переменной для чтения и записи, и хотя бы одна из операций является записью. Порядок выполнения операций непредсказуем, что ведет к недетерминированным и часто некорректным результатам.

Почему это происходит? Операции, которые кажутся атомарными (например, инкремент count++), на уровне процессора распадаются на несколько инструкций (чтение, изменение, запись). Потоки могут "пересекаться" на этих этапах.

Пример на Java:

class UnsafeCounter {
    private int count = 0;
    public void increment() {
        count++; // НЕ атомарная операция!
    }
    public int getCount() { return count; }
}
// Запуск двух потоков, вызывающих increment() 1000 раз каждый,
// может дать результат меньше 2000 из-за потерь обновлений.

Способы предотвращения:

  1. Синхронизация: Использование synchronized метода или блока.
    public synchronized void increment() { count++; }
  2. Атомарные типы из java.util.concurrent.atomic.
    private AtomicInteger count = new AtomicInteger(0);
    public void increment() { count.incrementAndGet(); }
  3. Иммутабельность: Использование неизменяемых объектов.
  4. Ограничение видимости: Локальные переменные или ThreadLocal.

Важно: Гонка данных — это проблема корректности, а не просто производительности.

Ответ 18+ 🔞

А, слушай, вот есть такая штука — гонка данных. Это, блядь, не Формула-1, а самая натуральная пиздец-ситуация в многопоточности. Представь: два потока, как два обдолбанных хомяка, несутся к одной и той же переменной. Один хочет её прочитать, другой — нахуярить туда новое значение. Или оба сразу писать. И вот этот момент, когда они сталкиваются лбами — это и есть гонка. Результат? А хуй его знает! Может сработать, а может выдать такую дичь, что потом неделю дебажить.

Почему так выходит, спросишь? Да потому что ты, мудак, думаешь, что count++ — это одна операция. Ан нет! На самом деле это три: "прочитай", "прибавь единичку", "запиши обратно". И вот пока один поток прибавляет, второй уже лезет читать старое значение. И пошло-поехало, обновления теряются, как носки в чёрной дыре стиральной машинки.

Вот, смотри, как это выглядит в коде, если делать по-простому, по-дурацки:

class UnsafeCounter {
    private int count = 0;
    public void increment() {
        count++; // Вот эта строчка — рассадник ебанины!
    }
    public int getCount() { return count; }
}
// Запустишь два потока, каждый по тысяче раз дернёт increment(),
// а в итоге получишь не 2000, а какую-нибудь хуйню вроде 1873. Красота!

Ну и как с этой хуйней бороться? Вариантов, слава богу, есть:

  1. Синхронизация. Вешаем замок synchronized. Типа "не лезь, пока я не закончил".
    public synchronized void increment() { count++; } // Теперь по одному, блядь, без давки!
  2. Атомарные штуки. Из пакета java.util.concurrent.atomic. Они там внутри всё по-умному делают.
    private AtomicInteger count = new AtomicInteger(0);
    public void increment() { count.incrementAndGet(); } // И никаких гонок, ёпта!
  3. Иммутабельность. Самый хитрый способ. Если объект нельзя изменить после создания, то и гоняться не за чем. Все данные — как влитые.
  4. Спрятать подальше. Использовать локальные переменные или ThreadLocal, чтобы у каждого потока была своя личная песочница, и они друг другу в ней не срали.

Запомни главное: гонка данных — это не про то, что программа будет медленнее работать. Это про то, что она будет работать НЕПРАВИЛЬНО. А это, блядь, уже не баг, а позор на всю итерацию.