Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Безопасность потока (Thread Safety)
Определение
Безопасность потока (Thread Safety) — это свойство программного кода, которое гарантирует корректное поведение, когда его выполняют несколько потоков одновременно, обращаясь к одним и тем же ресурсам (переменным, объектам, файлам и т.д.).
Простыми словами: если несколько потоков работают с одним объектом одновременно, он остаётся в консистентном состоянии и не происходит race conditions.
Проблема: Race Condition
Пример небезопасного кода
public class Counter {
private int count = 0;
public void increment() {
count++; // ЭТА ОПЕРАЦИЯ НЕ АТОМАРНА!
}
public int getCount() {
return count;
}
}
// Использование из двух потоков
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
// Ожидается 2000, но может быть 1876 или любое другое число!
Почему это происходит?
Операция count++ состоит из трёх шагов:
- Read: прочитать текущее значение count (например, 100)
- Increment: увеличить на 1 (100 + 1 = 101)
- Write: написать обратно (count = 101)
Если два потока выполняют это одновременно:
Поток 1 Поток 2
─────────────────────────────────────
Read count = 100
Read count = 100 (!!)
Increment → 101
Increment → 101 (!!)
Write count = 101
Write count = 101 (!!)
Результат: count = 101, хотя должно быть 102
Потеряно увеличение от одного потока!
Это и есть race condition.
Решения: Как сделать код thread-safe
1. Synchronized (базовый вариант)
public class ThreadSafeCounter1 {
private int count = 0;
public synchronized void increment() {
count++; // Теперь это атомарная операция
}
public synchronized int getCount() {
return count;
}
}
Как работает:
synchronizedзахватывает монитор (lock) объекта- Только один поток может находиться в synchronized методе одновременно
- Другие потоки ждут своей очереди
Минусы:
- Может быть медленнее на высоконагруженных системах
- Риск deadlock если не осторожен с порядком захвата локов
2. AtomicInteger (рекомендуется)
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadSafeCounter2 {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Атомарная операция
}
public int getCount() {
return count.get();
}
}
Преимущества:
- Быстрее чем synchronized
- Использует CAS (Compare-And-Swap) на уровне CPU
- Не приводит к deadlock
- Явно показывает intent (что работаем с многопоточностью)
3. ReentrantLock (гибкий вариант)
import java.util.concurrent.locks.ReentrantLock;
public class ThreadSafeCounter3 {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // ВСЕГДА отпускай lock, даже если exception
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
Когда использовать:
- Нужна гибкость (tryLock, timeout)
- Нужны Condition (wait/notify на стероидах)
4. Immutable объекты (идеальный вариант)
public class ImmutableCounter {
private final int count;
public ImmutableCounter(int count) {
this.count = count;
}
public ImmutableCounter increment() {
return new ImmutableCounter(count + 1); // Новый объект
}
public int getCount() {
return count; // Не нужно синхронизировать
}
}
// Использование (functional style)
ImmutableCounter counter = new ImmutableCounter(0);
counter = counter.increment(); // Создаётся новый объект
Почему безопасно:
- Данные не изменяются
- Каждая операция создаёт новый объект
- Потокам нечего синхронизировать
Минусы:
- Больше объектов в памяти
- Может быть медленнее для частых изменений
5. ThreadLocal (изоляция данных)
public class ThreadLocalExample {
// Каждый поток имеет свою копию переменной
private static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public String formatDate(Date date) {
return dateFormat.get().format(date); // Безопасно!
}
}
Когда полезно:
- Когда каждый поток работает с собственными данными
- SimpleDateFormat, Connection pools, и т.д.
6. Synchronized Collections
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
List<String> list = Collections.synchronizedList(new ArrayList<>());
list.add("item"); // Thread-safe
String item = list.get(0); // Thread-safe
Минус:
- Iteration всё ещё не безопасна:
for (String item : list) { // Может выброситься ConcurrentModificationException System.out.println(item); }
7. ConcurrentHashMap (лучше для Collections)
import java.util.concurrent.ConcurrentHashMap;
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("count", 0);
map.compute("count", (k, v) -> v + 1); // Атомарная операция
Преимущества:
- Thread-safe
- Лучше производительность чем synchronized HashMap
- Безопасная итерация
Правила безопасности потока
1. Синхронизируй доступ к shared mutable state
// Плохо
public class BadExample {
public int sharedCounter = 0; // Public, mutable!
}
// Хорошо
public class GoodExample {
private int sharedCounter = 0; // Private
public synchronized int getCounter() {
return sharedCounter;
}
}
2. Используй final для неизменяемых ссылок
public class Example {
private final List<String> items = new ArrayList<>();
// Ссылка на объект не может измениться, но сам список может
public synchronized void addItem(String item) {
items.add(item);
}
}
3. Помни про memory visibility
public class VisibilityExample {
private volatile boolean flag = false; // volatile = видно всем потокам
public void setFlag() {
flag = true; // Все потоки видят это изменение мгновенно
}
public boolean getFlag() {
return flag;
}
}
4. Избегай nested locks (deadlock)
// ОПАСНО: может быть deadlock
synchronized(lock1) {
synchronized(lock2) {
// Если два потока захватят их в разном порядке — deadlock
}
}
// Безопаснее: всегда одинаковый порядок
// Или лучше: избегай nested locks
Вещи которые ЯВЛЯЮТСЯ thread-safe по умолчанию
- Immutable объекты (String, Integer, и т.д.)
- final переменные примитивов
- Атомарные операции (read/write одного поля)
- Synchronized методы и блоки
- volatile переменные (для visibility)
- Concurrent коллекции (ConcurrentHashMap, CopyOnWriteArrayList)
Практический пример: Безопасный счётчик для веб приложения
import java.util.concurrent.atomic.AtomicLong;
@Service
public class RequestCounterService {
private final AtomicLong totalRequests = new AtomicLong(0);
private final AtomicLong totalErrors = new AtomicLong(0);
@Around("execution(* com.example.controller.*.*(..))") // AOP
public Object countRequest(ProceedingJoinPoint joinPoint) throws Throwable {
totalRequests.incrementAndGet();
try {
return joinPoint.proceed();
} catch (Exception e) {
totalErrors.incrementAndGet();
throw e;
}
}
public long getTotalRequests() {
return totalRequests.get();
}
public long getTotalErrors() {
return totalErrors.get();
}
}
Заключение
Безопасность потока — это критическое свойство для многопоточных приложений. Основные стратегии:
- Синхронизация — synchronized, locks
- Атомарные операции — AtomicInteger, и т.д.
- Immutability — неизменяемые объекты
- Изоляция — ThreadLocal, каждый поток свои данные
- Concurrent коллекции — ConcurrentHashMap, CopyOnWriteArrayList
Выбирай самый простой подход, который решает проблему, и избегай чрезмерной синхронизации, которая может привести к deadlock или плохой производительности.