Что такое потокобезопасный класс в Java?

Ответ

Потокобезопасный класс — это класс, внутреннее состояние которого остаётся консистентным и корректным при одновременном использовании из нескольких потоков без необходимости внешней синхронизации со стороны клиентского кода.

Ключевой признак: Инварианты класса не нарушаются в условиях конкурентного доступа (чтения и записи).

Пример НЕпотокобезопасного и потокобезопасного класса:

// НЕ потокобезопасный счётчик
class UnsafeCounter {
    private int value = 0;
    public void increment() { value++; } // Операция не атомарна!
    public int getValue() { return value; }
}

// Потокобезопасный счётчик с использованием synchronized
class SynchronizedCounter {
    private int value = 0;
    public synchronized void increment() { value++; } // Блокировка по экземпляру
    public synchronized int getValue() { return value; }
}

// Потокобезопасный счётчик с использованием AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
    private AtomicInteger value = new AtomicInteger(0);
    public void increment() { value.incrementAndGet(); } // Атомарная операция CAS
    public int getValue() { return value.get(); }
}

Основные стратегии обеспечения потокобезопасности:

  1. Полная инкапсуляция состояния и использование синхронизации (synchronized методы/блоки, ReentrantLock).
  2. Использование неизменяемых (immutable) объектов. Если состояние объекта не может измениться после создания, он по определению потокобезопасен.
  3. Использование потокобезопасных классов из java.util.concurrent:
    • AtomicInteger, AtomicReference (атомарные операции).
    • ConcurrentHashMap, CopyOnWriteArrayList (специализированные коллекции).
    • ThreadLocal (хранение состояния для каждого потока отдельно).
  4. Правильная публикация объектов (использование final полей, volatile переменных).

Важно: Потокобезопасность — это свойство всего класса, а не отдельных методов. Даже если каждый метод синхронизирован, композиция вызовов (например, «проверить-и-действовать») без внешней синхронизации может привести к состоянию гонки (race condition).

Ответ 18+ 🔞

Э, слушай сюда, ты, пизда с ушами! Сидишь тут, думаешь, что если у тебя класс с методом, то его можно из всех потоков сразу дёргать? Ёпта, сейчас я тебе мозги по полочкам разложу, что такое потокобезопасность, а то у тебя, я смотрю, подозрение ебать чувствую — ноль.

Вот представь, у тебя есть класс, как эта самая мартышлюшка, которая раздает бананы. Если к ней одновременно десять голодных зверей пристанут, а у неё банан один, что будет? Правильно, драка, пиздец и банан в жопе у гориллы. Так вот, потокобезопасный класс — это такая хитрая жопа-раздатчица, у которой на всех бананов хватает, и она так ловко ими жонглирует, что даже если вся стая на неё набросится, инварианты не нарушатся. То есть, всё внутри останется консистентным, и никто не получит хуй с горы вместо банана.

Ключевой признак: сколько потоков ни долбись в него, всё будет чётко. Никаких гонок (race condition), только хардкор.

Смотри, вот тебе живой пример, чтобы ты, блядь, проникся. Классика жанра — счётчик.

// Это твой пиздопроебибный, НЕ потокобезопасный счётчик. На нём табличка "Убьёт".
class UnsafeCounter {
    private int value = 0;
    public void increment() { value++; } // Операция не атомарна! Тут пиздец и начинается.
    public int getValue() { return value; }
}

// А это уже по-взрослому. Синхронизированный, то есть с замком на двери.
class SynchronizedCounter {
    private int value = 0;
    public synchronized void increment() { value++; } // Зашёл один — закрылся, сделал дело — вышел.
    public synchronized int getValue() { return value; }
}

// Ну и топчик, для тех, кто в теме. Атомики, детка. Быстро, модно, молодёжно.
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
    private AtomicInteger value = new AtomicInteger(0);
    public void increment() { value.incrementAndGet(); } // Всё за одну CPU-инструкцию, нихуя не проспоришь.
    public int getValue() { return value.get(); }
}

Видишь разницу? В первом случае value++ — это же, блядь, три операции: прочитать, увеличить, записать. И если два потока в промежуток влезут, они друг другу результат перетрут, и в итоге вместо двух инкрементов будет один. Овердохуища потерь!

Так как же эту безопасность достичь, спросишь ты? Стратегии, сука, есть:

  1. Засунуть всё состояние внутрь и поставить синхронизацию. synchronized, замки (ReentrantLock) — классика, но может тормозить, если бездумно применять.
  2. Сделать объект неизменяемым (immutable). Гениальная идея! Если состояние после создания менять нельзя, то хоть тысяча потоков читай — им всё равно. Потокобезопасность нахуй из коробки.
  3. Юзать готовые кирпичи из java.util.concurrent. Там умные люди уже всё за тебя придумали:
    • AtomicInteger, AtomicReference — для атомарных подебок.
    • ConcurrentHashMap, CopyOnWriteArrayList — коллекции, где можно почти без боли читать и писать одновременно.
    • ThreadLocal — чтоб каждый поток в своём углу со своим state-ом сидел и другим не мешал.
  4. Правильно публиковать объекты. Использовать final поля, volatile переменные, чтобы один поток, создавший объект, гарантированно показал его другим во всей красе, а не в каком-то промежуточном, ебучем состоянии.

И главное, запомни раз и нахуй: потокобезопасность — это свойство всего класса, а не отдельных его методов. Можно навесить synchronized на каждый метод, но если ты потом вызовешь два метода подряд (типа «проверить, а потом действовать»), то между ними другой поток влезет и всё опиздюрит. Это называется состояние гонки (race condition), и от него бомбит не по-детски.

Короче, думай головой, когда пишешь многопоточку. А то получится как в том анекдоте: «Работает? Не трожь!» — пока на прод не выкатишь и всё не накроется медным тазом.