Concurrent

Проработать

  1. Advanced Java - Concurrency
  2. Обзор java.util.concurrent.*
  3. Locks: блокировка доступа к ресурсам
  4. Atomic, Java Memory Model, Happens-Before, JCStress ~/ Многопоточность в Java☕
  5. Синхронизация потоков. Оператор synchronized в Java

Что это

Это пакет в java.util предназначенный для работы с многопоточность.

Основные разделы

Способы синхронизация

  1. wait/notify
  2. synchronized
  3. Thread.join
  4. Lock
  5. Semaphore

Synchronized

synchronized - ключевое слово, которое позволяет заблокировать доступ к методу или части кода(блок кода), если его уже использует другой поток. По принципу Mutex. Все остальные потоки которые попробуют получить монитор, станут wait(). После выхода из монитора вызывается - notify(). Не известно какой именно поток с ожиданием запуститься.

Применения

  1. Для блока кода - если не нужно синхронизировать весь метод. Нужно передавать объект в качестве монитора. Обычно передается this, это делает синхронизацию по текущему объекту.
    private Object key = new Object();
    
    synchronized (key) {
        System.out.println("Hi I'm synchronized block!");
    }
                    
  2. Для метода. В качестве объекта будет текущая ссылка на объект(this).
    synchronized void myMethod() {
        System.out.println("Hi I'm synchronized method!");
    }
                    

    Можно воспринимать так:

    void myMethod() {
        synchronized(this) {
          System.out.println("Hi I'm synchronized method!");
        }
    }
                    

    Для статического метода передается ".class". По этому статическая блокировка и не статическая, на одном классе не будут блокировать друг друга:

    static void myMethod() {
        synchronized(MyObject.class) {
          System.out.println("Hi I'm synchronized method!");
        }
    }
                    

Минус synchronized - другие потоки вынуждены ждать, пока нужный объект или метод освободится "bottle neck". Если у объекта два синхронизированных метода, два потока не могут одновременно зайти в два синхронизированных метода одного и того же объекта. Все потоки будут ждать освобождения общего лока.

Volatile

volatile - ключевое слово для переменной. Гарантирует видимость изменений между потоками. Указывает что переменная ожидает изменения многими потоками.

Atomic - дают атомарные операции + видимость.

  1. Не блокирует другие потоки.
  2. Не используется для инкремента.
  3. Она всегда будет атомарно read/write(не про атомарность измений). Даже если это 64-битные double или long. Несколько потоков не должны менять(edit) значение.
  4. Java-машина не будет помещать ее в кэш потоков(кеш процессора). Так что ситуация, когда 10 потоков работают со своими локальными копиями этой переменной - исключена.

Условия использования volatile:

Пример

public class VolatileExample {
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        // create and start a new thread
        new Thread(() -> {
            while (!flag) {
                // do some work
            }
            System.out.println("Thread finished");
        }).start();

        // set the flag to true after a delay
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
    }
}
        

Atomic

Пакет для атомарных(неделимые) изменений и операций, которые выполняются независимо и безопасно для многопоточности, без использования volatile, synchronized или Lock, что уменьшает риск Deadlock или Race conditions.

Часто используется как счетчик/генератор уникальных ID.

Compare and Swap(CAS) - механизм который используют Atomic классы. который поддерживается на уровне процессора.

Часто быстрее чем синхронизация или блокировка(lock-free). Оптимистическая блокировка.

Внутри помечены слово volatile.

Как работает

Виды:

  1. AtomicBoolean
  2. AtomicInteger
  3. AtomicLong
  4. AtomicIntegerArray
  5. AtomicLongArray
  6. AtomicReference
  7. AtomicReferenceArray
  8. AtomicStampedReference
  9. AtomicMarkableReference

Основные методы:

public class Counter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // атомарное ++
    }

    public int get() {
        return count.get();
    }
}
        

ABA-проблема

Значение могло измениться на «A → B → A». Для простых счётчиков это не критично, но в сложных структурах данных используют AtomicStampedReference. Необходимо только если значение может отниматься.

AtomicInteger когда использовать

Не подходит:

Virtual threads

Это лёгкие потоки, которые управляются самой JVM, а не операционной системой.

Потоки которые не требуют маппинга на реальные потоки процессора.

Легковесные потоки, которые позволяют писать синхронный код с производительностью асинхронного.

Классические Platform Threads имеют проблемы:

Virtual Threads

Ключевые моменты

Под капотом

Подводные камни

Pinning (закрепление)

Virtual thread может "застрять" на платформенном потоке при:

В таких случаях carrier thread блокируется вместе с virtual thread. Решение: использовать ReentrantLock вместо synchronized.

ThreadLocal может быть опасен

Миллионы virtual threads с ThreadLocal приведут к огромному потреблению памяти. Используйте ScopedValue (preview feature в Java 21+).

Не подходит для CPU-bound задач

Virtual threads оптимизированы для IO-bound операций. Для вычислений лучше параллельные стримы или ForkJoinPool.

Мониторинг

Стандартные инструменты мониторинга потоков могут показывать некорректные данные — они заточены под platform threads.

Когда использовать

Не подходит:

Virtual Threads — мощный инструмент, который не заменяет потоки ОС, но отлично подходит для массовых I/O-задач. Если у вас есть сервис, работающий с сетью, базами данных или API — пора внедрять Virtual Threads.