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

Как обеспечить потокобезопасность переменной без использования атомарных классов

2.3 Middle🔥 231 комментариев
#Многопоточность

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

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

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

Ответ

Потокобезопасность переменной без использования атомарных классов можно обеспечить несколькими способами. Рассмотрю основные подходы от простых к сложным.

1. Synchronized методы

Простой и понятный способ синхронизации:

public class Counter {
    private int count = 0;
    
    // Синхронизированный метод
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

// Использование
Counter counter = new Counter();
for (int i = 0; i < 1000; i++) {
    counter.increment();
}
System.out.println("Count: " + counter.getCount());  // 1000

2. Synchronized блоки

Более гибкий способ — синхронизация части кода:

public class BankAccount {
    private double balance = 0;
    
    public void deposit(double amount) {
        synchronized (this) {
            balance += amount;
        }
    }
    
    public void withdraw(double amount) {
        synchronized (this) {
            if (balance >= amount) {
                balance -= amount;
            }
        }
    }
    
    public double getBalance() {
        synchronized (this) {
            return balance;
        }
    }
}

3. Volatile ключевое слово

Для простых переменных, когда нет race conditions между операциями:

public class Flag {
    // volatile гарантирует видимость изменений между потоками
    private volatile boolean running = true;
    
    public void stop() {
        running = false;  // Видно всем потокам сразу
    }
    
    public boolean isRunning() {
        return running;
    }
}

// Использование
Flag flag = new Flag();

Thread workerThread = new Thread(() -> {
    while (flag.isRunning()) {
        // Работа
    }
});

workerThread.start();
Thread.sleep(1000);
flag.stop();  // Поток завершится
workerThread.join();

4. ReadWriteLock — для большого объёма чтений

Оптимально, когда чтений намного больше чем записей:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Cache {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private String value = "";
    
    public void setValue(String val) {
        lock.writeLock().lock();
        try {
            value = val;
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public String getValue() {
        lock.readLock().lock();
        try {
            return value;
        } finally {
            lock.readLock().unlock();
        }
    }
}

5. ReentrantLock — явное управление блокировкой

Больше контроля чем synchronized:

import java.util.concurrent.locks.ReentrantLock;

public class Queue {
    private final ReentrantLock lock = new ReentrantLock();
    private java.util.Queue<String> queue = new java.util.LinkedList<>();
    
    public void enqueue(String item) {
        lock.lock();
        try {
            queue.add(item);
        } finally {
            lock.unlock();  // Обязательно в finally
        }
    }
    
    public String dequeue() {
        lock.lock();
        try {
            return queue.poll();
        } finally {
            lock.unlock();
        }
    }
}

6. CopyOnWriteArrayList — для частых чтений

Для коллекций, когда мутирование редкое:

import java.util.concurrent.CopyOnWriteArrayList;

public class EventListener {
    // Создаёт копию при каждой записи, но чтение очень быстро
    private final CopyOnWriteArrayList<String> listeners = 
        new CopyOnWriteArrayList<>();
    
    public void addListener(String listener) {
        listeners.add(listener);  // Потокобезопасно
    }
    
    public void notifyListeners() {
        for (String listener : listeners) {  // Безопасно, даже если добавляют
            System.out.println("Notify: " + listener);
        }
    }
}

7. Collections.synchronizedMap/List

Для обёртывания обычных коллекций:

import java.util.Collections;
import java.util.Map;
import java.util.List;

public class SynchronizedCollections {
    
    public static void main(String[] args) {
        // Синхронизированная Map
        Map<String, Integer> map = 
            Collections.synchronizedMap(new HashMap<>());
        map.put("count", 1);
        
        // Синхронизированный List
        List<String> list = 
            Collections.synchronizedList(new ArrayList<>());
        list.add("item");
        
        // Синхронизированный Set
        Set<String> set = 
            Collections.synchronizedSet(new HashSet<>());
        set.add("value");
    }
}

8. Double-Checked Locking (для ленивой инициализации)

Эффективный паттерн инициализации:

public class Singleton {
    // volatile критична для DCL
    private volatile static Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {  // Первая проверка без блокировки
            synchronized (Singleton.class) {
                if (instance == null) {  // Вторая проверка в блокировке
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

9. Thread-local переменные

Для данных, уникальных для каждого потока:

public class ThreadSafeData {
    // Каждый поток имеет свою копию
    private static final ThreadLocal<String> userIdHolder = 
        ThreadLocal.withInitial(() -> null);
    
    public static void setUserId(String userId) {
        userIdHolder.set(userId);
    }
    
    public static String getUserId() {
        return userIdHolder.get();
    }
    
    public static void clearUserId() {
        userIdHolder.remove();  // Важно для предотвращения утечек
    }
}

// Использование
public class Request {
    public void handle(String userId) {
        ThreadSafeData.setUserId(userId);
        try {
            process();
        } finally {
            ThreadSafeData.clearUserId();  // Всегда очищать
        }
    }
}

10. Immutable объекты

Потокобезопасность через неизменяемость:

public class ImmutableUser {
    private final String name;
    private final int age;
    
    public ImmutableUser(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    // Никогда не меняется — полностью потокобезопасна
}

// Если нужно обновить — создаём новый объект
ImmutableUser user = new ImmutableUser("John", 30);
ImmutableUser updatedUser = new ImmutableUser("John", 31);

11. Confinement — данные ограничены одним потоком

public class ThreadConfinement {
    private final List<String> userMessages = new ArrayList<>();n    
    public void processUser() {
        // Все операции с userMessages только в одном потоке
        userMessages.add("Message");
        userMessages.forEach(System.out::println);
    }
    
    // Никогда не передаём userMessages другим потокам
}

Сравнение подходов

ПодходИспользованиеПлюсыМинусы
synchronizedПростое блокированиеПростоМожет быть медленным
volatileВидимостьБыстроНе для сложных операций
ReadWriteLockМного чтенийОптимальноСложнее
ReentrantLockЯвный контрольГибкоНадо не забывать unlock
CopyOnWriteArrayListСпискиБезопасноДорого при записи
ThreadLocalДанные потокаПростоУтечки памяти
ImmutableПостоянные данныеПолностью безопасноБольше объектов

Лучшие практики

  1. Начните с synchronized — просто и надёжно
  2. Используйте volatile для флагов видимости
  3. Immutable объекты — лучше чем синхронизация
  4. Избегайте синхронизации если возможно
  5. Профилируйте перед оптимизацией
  6. Всегда очищайте ThreadLocal в finally блоках
  7. Предпочитайте ReentrantLock для сложных сценариев

Выбор метода зависит от конкретного сценария. Для большинства случаев достаточно synchronized методов или volatile переменных.