Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Синхронизация в Java: Многопоточность и потокобезопасность
Синхронизация (synchronization) — это механизм контроля доступа к общим ресурсам (переменным, объектам) в многопоточной среде. Она гарантирует, что только один поток может выполнять критический раздел кода одновременно, предотвращая race conditions и обеспечивая consistency данных.
Проблема: Race Condition
// ❌ Без синхронизации — race condition
public class Counter {
private int count = 0; // Общий ресурс
public void increment() {
count++; // НЕ атомарная операция!
// В реальности это: read -> increment -> write
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for(int i = 0; i < 10000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for(int i = 0; i < 10000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count); // Ожидаем 20000
// Но выведет что-то вроде: 15843, 18421, 19997 — НЕПРАВИЛЬНО!
}
}
// Почему происходит race condition:
// Thread 1: read count=5 -> increment -> write count=6
// Thread 2: read count=5 -> increment -> write count=6
// (прочитал старое значение, пока Thread 1 его обновлял)
Решение 1: synchronized методы
// ✅ С синхронизацией
public class SynchronizedCounter {
private int count = 0;
// synchronized гарантирует, что одновременно в методе
// может находиться только один поток
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
Thread t1 = new Thread(() -> {
for(int i = 0; i < 10000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for(int i = 0; i < 10000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount()); // 20000 ✅ ПРАВИЛЬНО!
}
}
Как работает synchronized
public synchronized void method() {
// Когда поток входит в synchronized метод:
// 1. Пытается захватить монитор объекта (mutex/lock)
// 2. Если другой поток его держит — ждёт в очереди
// 3. Выполняет код метода
// 4. Освобождает монитор при выходе
}
// Это эквивалентно:
public void method() {
synchronized(this) {
// Критический раздел
}
}
Решение 2: synchronized блоки (более гибко)
public class Counter {
private int count = 0;
private Object lock = new Object(); // Отдельный объект-монитор
// Синхронизируем только нужную часть
public void increment() {
// Обработка без синхронизации (быстро)
int temp = calculateValue();
// Синхронизируем только критический раздел
synchronized(lock) {
count += temp;
}
// Больше кода без синхронизации
doSomethingElse();
}
public int getCount() {
synchronized(lock) {
return count;
}
}
private int calculateValue() { return 1; }
private void doSomethingElse() { }
}
Решение 3: Современные инструменты
AtomicInteger (рекомендуется)
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Атомарная операция
}
public int getCount() {
return count.get();
}
}
ReentrantLock (более гибкий)
import java.util.concurrent.locks.ReentrantLock;
public class LockCounter {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // Важно разблокировать даже при исключении
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
// С try-with-resources (если использовать ReadWriteLock)
try(var ignored = lock.lock()) {
count++;
}
Visibility: volatile
Атомарность ≠ Видимость
// ❌ Проблема видимости
public class VisibilityProblem {
private boolean flag = false;
public void thread1() {
// Поток 1 может кэшировать значение flag
while(!flag) {
// Может бесконечно ждать, хотя flag изменилась
}
}
public void thread2() {
flag = true; // Изменение может быть не видно thread1
}
}
// ✅ Решение: volatile
public class VisibilityFixed {
private volatile boolean flag = false; // Видимо между потоками
public void thread1() {
while(!flag) {
// Будет видеть изменения из thread2
}
}
public void thread2() {
flag = true; // Сразу видимо всем потокам
}
}
Memory Model: Happens-Before
// synchronized гарантирует happens-before отношение
public class HappensBefore {
private int x = 0;
private boolean ready = false;
public synchronized void write() {
x = 42;
ready = true;
// ← synchronized гарантирует:
// все записи в памяти упорядочены
}
public synchronized void read() {
if(ready) {
System.out.println(x); // Гарантированно 42, не 0
// synchronized гарантирует видимость
}
}
}
Дедлоки: опасность синхронизации
// ❌ Дедлок
public class DeadlockExample {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void method1() {
synchronized(lock1) {
// Поток A здесь
try { Thread.sleep(100); } catch(InterruptedException e) {}
synchronized(lock2) {
// Ждёт lock2...
}
}
}
public void method2() {
synchronized(lock2) {
// Поток B здесь
try { Thread.sleep(100); } catch(InterruptedException e) {}
synchronized(lock1) {
// Ждёт lock1... ДЕДЛОК!
// Поток A ждёт lock2 (которую держит B)
// Поток B ждёт lock1 (которую держит A)
}
}
}
}
// ✅ Правило: всегда захватывай монаторы в одинаковом порядке
public class NoDeadlock {
private Object lock1 = new Object();
private Object lock2 = new Object();
// Всегда lock1 -> lock2
public void method1() {
synchronized(lock1) {
synchronized(lock2) {
// Безопасно
}
}
}
public void method2() {
synchronized(lock1) { // Сначала lock1
synchronized(lock2) { // Потом lock2
// Безопасно
}
}
}
}
Сравнение подходов
| Подход | Преимущества | Недостатки | Когда использовать |
|---|---|---|---|
| synchronized | Простой, встроенный | Грубый, может быть медленным | Простые случаи |
| synchronized блок | Гибче, быстрее | Требует аккуратности | Критические секции |
| AtomicInteger | Быстро, modern | Только для чисел | Счётчики, флаги |
| ReentrantLock | Очень гибко (tryLock, conditions) | Сложнее, нужно unlock | Сложные сценарии |
| ReadWriteLock | Много читателей | Более сложно | Много read, мало write |
Thread-safe Collections
// ❌ Не безопасно
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
// Если несколько потоков обращаются одновременно — race condition
// ✅ Безопасно (Java 8+)
List<String> list = Collections.synchronizedList(new ArrayList<>());
// или (лучше)
List<String> list = new CopyOnWriteArrayList<>();
// ✅ Безопасно
Map<String, Integer> map = new ConcurrentHashMap<>();
// или
Set<String> set = Collections.newSetFromMap(new ConcurrentHashMap<>());
Best Practices
// 1. Минимизируй critical section
public synchronized void badMethod() {
// Много кода здесь
doExpensiveCalculation();
updateDatabase();
log();
}
// ✅ Лучше
public void goodMethod() {
int result = doExpensiveCalculation(); // Без синхронизации
synchronized(this) { // Только нужное
data = result;
}
updateDatabase(); // Без синхронизации
log(); // Без синхронизации
}
// 2. Предпочитай modern инструменты
// ❌ synchronized int count = 0;
// ✅ AtomicInteger count = new AtomicInteger(0);
// 3. Избегай вложенных блокировок
// ❌ synchronized(lock1) { synchronized(lock2) { ... } }
// ✅ synchronized(lock1) { ... }
// 4. Используй volatile для флагов видимости
private volatile boolean shutdownRequested = false;
// 5. Документируй какие поля защищены
private int count; // Защищено lock'ом
private final AtomicInteger atomic = new AtomicInteger();
Заключение
- Синхронизация — необходимо для безопасной многопоточности
- synchronized — простой встроенный механизм
- Atomics — современный и быстрый способ
- Locks — гибкий, но требует аккуратности
- volatile — для видимости, не для атомарности
- Дедлоки — всегда захватывай в одном порядке
- ConcurrentCollections — используй готовые thread-safe структуры