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

С какими объектами можно работать многопоточно

2.2 Middle🔥 81 комментариев
#Основы Java

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

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

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

Работа с многопоточными объектами в Java

Многопоточность — способность приложения выполнять несколько операций одновременно. Однако не все объекты изначально потокобезопасны. Разберёмся, какие объекты можно безопасно использовать в многопоточной среде.

1. Thread-safe объекты из java.util.concurrent

ConcurrentHashMap

Лучшая альтернатива обычному HashMap для многопоточной среды:

// ❌ Плохо: HashMap не потокобезопасен
Map<String, Integer> unsafeMap = new HashMap<>();
unsafeMap.put("counter", 1); // Race condition при одновременных операциях

// ✅ Хорошо: ConcurrentHashMap использует segment-based locking
ConcurrentHashMap<String, Integer> safeMap = new ConcurrentHashMap<>();
safeMap.put("counter", 1);

ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
    final int iteration = i;
    executor.submit(() -> {
        safeMap.putIfAbsent("user_" + iteration, iteration);
        Integer value = safeMap.get("user_" + iteration);
    });
}
executor.shutdown();

Как работает: ConcurrentHashMap разделяет данные на segments, каждый с отдельной блокировкой. Множество потоков могут одновременно читать и писать в разные segments.

Операции:

  • putIfAbsent() — атомарно добавит только если ключ не существует
  • computeIfAbsent() — вычисляет значение только при необходимости

CopyOnWriteArrayList

Для часто читаемых, редко изменяемых списков:

// Много читателей, редко добавляем элементы
List<String> eventLog = new CopyOnWriteArrayList<>();

// 1000 потоков читают
for (int i = 0; i < 1000; i++) {
    new Thread(() -> {
        for (String event : eventLog) { // Безопасное чтение
            System.out.println(event);
        }
    }).start();
}

// Один поток пишет
new Thread(() -> {
    eventLog.add("Event 1"); // Создаёт копию всего списка внутри
    eventLog.add("Event 2");
}).start();

Внутри: При записи создаёт полную копию массива, поэтому:

  • Чтение очень быстро (не требует синхронизации)
  • Запись медленная (копирует всё)
  • Хорошо для конфигов, которые часто читаются

BlockingQueue

Для коммуникации между потоками:

// Producer-Consumer паттерн
BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<>(100);

// Producer потоки
ExecutorService producerPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
    producerPool.submit(() -> {
        try {
            Task task = generateTask();
            taskQueue.put(task); // Блокируется если очередь полна
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}

// Consumer потоки
ExecutorService consumerPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
    consumerPool.submit(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                Task task = taskQueue.take(); // Ждёт если очередь пуста
                processTask(task);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    });
}

Типы BlockingQueue:

  • LinkedBlockingQueue — unbounded очередь
  • ArrayBlockingQueue — fixed-size очередь
  • PriorityBlockingQueue — очередь с приоритетами
  • SynchronousQueue — для direct handoff (нет буфера)

ConcurrentLinkedQueue

Для очереди без блокировок (lock-free):

ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();

// Много потоков добавляют
for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        for (int j = 0; j < 100; j++) {
            queue.offer("item_" + Thread.currentThread().getId() + "_" + j);
        }
    }).start();
}

// Читают
while (!queue.isEmpty()) {
    String item = queue.poll();
    System.out.println("Processing: " + item);
}

2. Неизменяемые объекты (Immutable Objects)

Иммутабельные объекты по природе потокобезопасны, потому что не могут быть изменены:

// ✅ Потокобезопасно: String неизменяем
String userName = "Alice";

for (int i = 0; i < 100; i++) {
    new Thread(() -> {
        System.out.println("User: " + userName); // Всегда "Alice"
    }).start();
}

// ✅ Создаём собственный immutable объект
public final class User {
    private final String id;
    private final String email;
    private final List<String> permissions; // Завёрнут в Collections.unmodifiableList
    
    public User(String id, String email, List<String> permissions) {
        this.id = id;
        this.email = email;
        // Защита от изменений снаружи
        this.permissions = Collections.unmodifiableList(new ArrayList<>(permissions));
    }
    
    public String getId() { return id; }
    public String getEmail() { return email; }
    public List<String> getPermissions() { return permissions; }
    
    // Нет setters!
}

// Безопасное создание новой версии (builder pattern)
public final class UserBuilder {
    private String id;
    private String email;
    private List<String> permissions = new ArrayList<>();
    
    public UserBuilder withEmail(String email) {
        this.email = email;
        return this;
    }
    
    public User build() {
        return new User(id, email, permissions);
    }
}

User user = new UserBuilder()
    .withEmail("alice@example.com")
    .build();

3. Синхронизированные коллекции (Collections.synchronized*)

Плохой вариант, но иногда нужен:

// ❌ Избегать: использует synchronized на весь объект
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());

// Проблема: итерация должна быть явно синхронизирована
for (String item : syncList) { // Может быть ConcurrentModificationException
    System.out.println(item);
}

// Правильный способ с synchronized коллекциями:
synchronized(syncList) {
    for (String item : syncList) { // Теперь безопасно
        System.out.println(item);
    }
}

// Но лучше использовать ConcurrentHashMap и друзья!

4. Atomic классы (java.util.concurrent.atomic)

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

public class Counter {
    // ✅ Потокобезопасный счётчик
    private AtomicInteger count = new AtomicInteger(0);
    private AtomicLong requestTime = new AtomicLong(0);
    private AtomicReference<String> currentStatus = new AtomicReference<>("idle");
    
    public void increment() {
        count.incrementAndGet(); // Атомарная операция
    }
    
    public int getCount() {
        return count.get();
    }
    
    public void recordTime(long duration) {
        requestTime.addAndGet(duration); // Атомарное сложение
    }
    
    public boolean changeStatus(String from, String to) {
        return currentStatus.compareAndSet(from, to); // CAS операция
    }
}

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

ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
    executor.submit(counter::increment);
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);

System.out.println("Final count: " + counter.getCount()); // Всегда 1000

Atomic операции:

  • getAndSet() — получить текущее, установить новое
  • compareAndSet() — CAS операция (compare-and-swap)
  • incrementAndGet() — атомарное увеличение
  • updateAndGet() — применить функцию

5. ThreadLocal (Осторожно!)

Для per-thread состояния:

public class RequestContext {
    private static ThreadLocal<String> userIdHolder = ThreadLocal.withInitial(() -> null);
    private static ThreadLocal<String> requestIdHolder = ThreadLocal.withInitial(UUID::randomUUID);
    
    public static void setUserId(String userId) {
        userIdHolder.set(userId);
    }
    
    public static String getUserId() {
        return userIdHolder.get();
    }
    
    public static void clean() {
        userIdHolder.remove(); // ВАЖНО: очистить, иначе memory leak в thread pool
        requestIdHolder.remove();
    }
}

// Spring Filter для web запросов
@Component
public class RequestContextFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        try {
            RequestContext.setUserId(getCurrentUserId());
            chain.doFilter(request, response);
        } finally {
            RequestContext.clean(); // Очистка!
        }
    }
}

Опасность ThreadLocal:

  • Утечка памяти если не очистить в thread pool
  • Сложно отследить значения при debugging
  • Неправильное использование в async коде

6. Явная синхронизация (когда нужна гибкость)

public class BankAccount {
    private double balance;
    private final Object lock = new Object();
    
    public void transfer(double amount) {
        synchronized(lock) { // Явная блокировка на объект
            if (balance >= amount) {
                balance -= amount; // Критическая секция
            }
        }
    }
    
    public double getBalance() {
        synchronized(lock) {
            return balance;
        }
    }
}

// Или с ReentrantLock для большей гибкости
public class ReentrantBankAccount {
    private double balance;
    private final Lock lock = new ReentrantLock();
    private final Condition sufficientFunds = lock.newCondition();
    
    public void withdraw(double amount) throws InterruptedException {
        lock.lock();
        try {
            while (balance < amount) {
                sufficientFunds.await(); // Ждём пока будут деньги
            }
            balance -= amount;
            sufficientFunds.signalAll(); // Уведомляем waiting потоки
        } finally {
            lock.unlock();
        }
    }
    
    public void deposit(double amount) {
        lock.lock();
        try {
            balance += amount;
            sufficientFunds.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

Сравнение и выбор

ОбъектПотокобезопасенИспользованиеПроизводительность
HashMapSingle threadОчень быстро
HashtableРедко, legacyМедленно (весь lock)
ConcurrentHashMapМногопотокБыстро (segment lock)
Collections.synchronizedMapРедкоМедленно
CopyOnWriteArrayListМного readsМедленно на write
BlockingQueueProducer-consumerЗависит от impl
Atomic*Простые типыОчень быстро
ThreadLocalPer-threadБыстро
Immutable objectsДанныеЗависит от размера

Best Practices

  1. Предпочитай java.util.concurrent вместо синхронизации вручную
  2. Используй ConcurrentHashMap вместо synchronized maps
  3. Избегай lock contention — узкие места могут зависнуть приложение
  4. Помни про memory visibility — volatile, synchronized, atomic гарантируют
  5. Тестируй под нагрузкой — race conditions проявляются редко
  6. ThreadLocal требует cleanup в thread pool окружении
  7. Immutable объекты по умолчанию когда возможно