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

Как можно избежать проблем при работе нескольких потоков с одним ресурсом?

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

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

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

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

Ответ

Как избежать проблем при работе нескольких потоков с одним ресурсом

Это классическая проблема многопоточности. При одновременном доступе нескольких потоков к общему ресурсу возникают race conditions, deadlocks и потеря данных. Рассмотрим основные способы защиты.

1. Синхронизация (Synchronization)

Synchronized метод

public class Counter {
    private int count = 0;
    
    // Синхронизированный метод - доступ только одному потоку за раз
    public synchronized void increment() {
        count++; // Атомарная операция для одного потока
    }
    
    public synchronized int getCount() {
        return count;
    }
    
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        
        // 5 потоков инкрементируют счётчик 1000 раз
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }
        
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("Final count: " + counter.getCount()); // 5000 (всегда)
    }
}

Synchronized блок

public class BankAccount {
    private double balance = 1000;
    private Object lock = new Object();
    
    public void transfer(BankAccount to, double amount) {
        // Синхронизируем только критическую секцию
        synchronized (lock) {
            if (this.balance >= amount) {
                this.balance -= amount;
                to.balance += amount;
                System.out.println("Transferred " + amount);
            }
        }
    }
    
    public synchronized double getBalance() {
        return balance;
    }
}

2. Volatile переменные

Для простых случаев с единственной переменной:

public class VolatileExample {
    // volatile гарантирует видимость изменений всем потокам
    private volatile boolean running = true;
    private volatile int count = 0;
    
    public void reader() {
        while (running) {
            System.out.println("Current count: " + count);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    
    public void writer() {
        for (int i = 0; i < 100; i++) {
            count = i;
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        running = false;
    }
    
    public static void main(String[] args) throws InterruptedException {
        VolatileExample example = new VolatileExample();
        
        Thread writerThread = new Thread(example::writer);
        Thread readerThread = new Thread(example::reader);
        
        writerThread.start();
        readerThread.start();
        
        writerThread.join();
        readerThread.join();
    }
}

ВАЖНО: volatile работает только для видимости изменений, не для атомарности операций!

3. Atomic классы (рекомендуется)

Для простых операций над примитивами:

import java.util.concurrent.atomic.*;

public class AtomicCounter {
    // AtomicInteger гарантирует атомарные операции
    private AtomicInteger count = new AtomicInteger(0);
    private AtomicReference<String> lastUpdate = new AtomicReference<>("None");
    
    public void increment() {
        count.incrementAndGet();  // Атомарно
    }
    
    public void add(int value) {
        count.addAndGet(value);   // Атомарно
    }
    
    public int getCount() {
        return count.get();
    }
    
    public void updateLastAccess(String user) {
        lastUpdate.set(user);
    }
    
    public static void main(String[] args) throws InterruptedException {
        AtomicCounter counter = new AtomicCounter();
        
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }
        
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("Final count: " + counter.getCount()); // Всегда 5000
    }
}

4. ReentrantLock (явная блокировка)

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

import java.util.concurrent.locks.*;

public class BankAccountWithLock {
    private double balance = 1000;
    private final Lock lock = new ReentrantLock();
    
    public void deposit(double amount) {
        lock.lock();
        try {
            balance += amount;
            System.out.println("Deposited: " + amount);
        } finally {
            lock.unlock();
        }
    }
    
    public void withdraw(double amount) throws InterruptedException {
        // Попытка заблокироваться с таймаутом
        if (lock.tryLock(5, TimeUnit.SECONDS)) {
            try {
                if (balance >= amount) {
                    balance -= amount;
                    System.out.println("Withdrew: " + amount);
                }
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("Could not acquire lock");
        }
    }
    
    public double getBalance() {
        lock.lock();
        try {
            return balance;
        } finally {
            lock.unlock();
        }
    }
}

5. ReadWriteLock

Для случаев, когда чтение намного чаще, чем запись:

import java.util.concurrent.locks.*;

public class CachedUserRepository {
    private java.util.Map<Integer, String> cache = new java.util.HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public String getUser(int id) {
        lock.readLock().lock();
        try {
            return cache.get(id);
        } finally {
            lock.readLock().unlock();
        }
    }
    
    public void updateUser(int id, String name) {
        lock.writeLock().lock();
        try {
            cache.put(id, name);
        } finally {
            lock.writeLock().unlock();
        }
    }
}

6. Collections.synchronizedX

Для потокобезопасных коллекций:

import java.util.*;

public class SynchronizedCollectionsExample {
    public static void main(String[] args) {
        // Потокобезопасный список
        List<String> syncList = Collections.synchronizedList(new ArrayList<>());
        
        // Потокобезопасное множество
        Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
        
        // Потокобезопасная карта
        Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
        
        // Добавляем из нескольких потоков
        Thread[] threads = new Thread[3];
        for (int i = 0; i < 3; i++) {
            final int id = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    syncList.add("Thread-" + id + "-Item-" + j);
                }
            });
            threads[i].start();
        }
        
        // Ждём завершения
        for (Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        
        System.out.println("Total items: " + syncList.size()); // 300
    }
}

7. ConcurrentHashMap (лучший выбор)

Оптимиза для высокой конкурентности:

import java.util.concurrent.*;

public class ConcurrentHashMapExample {
    public static void main(String[] args) throws InterruptedException {
        // Поддерживает одновременное чтение и запись из разных потоков
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        
        // 4 потока пишут
        Thread[] writers = new Thread[4];
        for (int i = 0; i < 4; i++) {
            final int id = i;
            writers[i] = new Thread(() -> {
                for (int j = 0; j < 250; j++) {
                    map.put("key-" + id + "-" + j, j);
                }
            });
            writers[i].start();
        }
        
        // 2 потока читают
        Thread[] readers = new Thread[2];
        for (int i = 0; i < 2; i++) {
            readers[i] = new Thread(() -> {
                for (String key : map.keySet()) {
                    map.get(key);
                }
            });
            readers[i].start();
        }
        
        for (Thread t : writers) t.join();
        for (Thread t : readers) t.join();
        
        System.out.println("Total entries: " + map.size());
    }
}

8. Immutable объекты

Переиспользование вместо изменения:

import java.util.Collections;

public class User {
    private final String name;
    private final int age;
    
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    // Immutable объекты полностью потокобезопасны!
    public static void main(String[] args) {
        User user = new User("John", 30);
        // Несколько потоков могут безопасно читать одного и того же пользователя
    }
}

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

СпособПроизводительностьСложностьКогда использовать
synchronizedУмереннаяПростойПростые случаи
VolatileВысокаяПростойФлаги, счётчики (не часто)
Atomic*ВысокаяПростойЧисловые счётчики, ссылки
ReentrantLockХорошаяСредняяНужен контроль, таймауты
ReadWriteLockОтличная (много читателей)СредняяМного чтений, мало записей
ConcurrentHashMapОтличнаяСредняяВысокая конкурентность, collections
ImmutableОтличнаяПростойКогда можно сделать неизменяемым

Общие правила

  1. Immutability first — если можно сделать объект неизменяемым, делай это
  2. Используй высокоуровневые API — ConcurrentHashMap вместо HashMap + synchronized
  3. Избегай deadlock-ов — всегда захватывай ресурсы в одном порядке
  4. Минимизируй критическую секцию — синхронизируй только необходимое
  5. Предпочитай Atomic классы — для простых типов данных
  6. Testing — используй stress-тесты и инструменты вроде ThreadSanitizer

Пример: потокобезопасный сервис

public class UserService {
    private final ConcurrentHashMap<Integer, User> users = new ConcurrentHashMap<>();
    
    public void addUser(Integer id, User user) {
        users.put(id, user);
    }
    
    public User getUser(Integer id) {
        return users.get(id);
    }
    
    public void removeUser(Integer id) {
        users.remove(id);
    }
    
    public int getUserCount() {
        return users.size();
    }
}

Этот сервис полностью потокобезопасен без явной синхронизации!