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

Что такое безопасность потока?

2.0 Middle🔥 201 комментариев
#Безопасность

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

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

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

# Безопасность потока (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++ состоит из трёх шагов:

  1. Read: прочитать текущее значение count (например, 100)
  2. Increment: увеличить на 1 (100 + 1 = 101)
  3. 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();
    }
}

Заключение

Безопасность потока — это критическое свойство для многопоточных приложений. Основные стратегии:

  1. Синхронизация — synchronized, locks
  2. Атомарные операции — AtomicInteger, и т.д.
  3. Immutability — неизменяемые объекты
  4. Изоляция — ThreadLocal, каждый поток свои данные
  5. Concurrent коллекции — ConcurrentHashMap, CopyOnWriteArrayList

Выбирай самый простой подход, который решает проблему, и избегай чрезмерной синхронизации, которая может привести к deadlock или плохой производительности.