← Назад к вопросам
Как можно избежать проблем при работе нескольких потоков с одним ресурсом?
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 | Отличная | Простой | Когда можно сделать неизменяемым |
Общие правила
- Immutability first — если можно сделать объект неизменяемым, делай это
- Используй высокоуровневые API — ConcurrentHashMap вместо HashMap + synchronized
- Избегай deadlock-ов — всегда захватывай ресурсы в одном порядке
- Минимизируй критическую секцию — синхронизируй только необходимое
- Предпочитай Atomic классы — для простых типов данных
- 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();
}
}
Этот сервис полностью потокобезопасен без явной синхронизации!