Concurrent

31.12.2012https://habr.com/ru/post/164487
16.01.2024https://dev.to/faangmaster/vopros-s-sobiesiedovaniia-chto-takoie-java-memory-model-i-happens-before-410g

Thread safe

Потокобезопасный. Может быть безопасно вызван из любого потока.

Process

Процесс - совокупность кода и данных разделяющих общее виртуальное адресное пространство. При помощи процессов выполнение разных программ изолировано друг от друга: каждое приложение использует свою область памяти, не мешая другим программам. Чаще всего одна программа состоит из одного процесса, но бывают и исключения (например, браузер Chrome создает отдельный процесс для каждой вкладки, что дает ему некоторые преимущества, вроде независимости вкладок друг от друга). Процессы изолированы друг от друга, поэтому прямой доступ к памяти чужого процесса невозможен (взаимодействие между процессами осуществляется с помощью специальных средств).

• если запущен процесс - в нем запущен хотя бы 1 поток.

• процесс завершается когда завершается его последний поток.

Thread

Поток - небольшая часть процесса, последовательность инструкций, которая выполняется параллельно с другими потоками. Каждое приложение создает как минимум один поток: основной, который запускает функцию main(). Один поток – это одна единица исполнения кода. Каждый поток последовательно выполняет инструкции процесса, которому он принадлежит, параллельно с другими потоками этого процесса.

Асинхронность

Концепция программирования, когда события происходят независимо от главного потока и способ взаимодействия не является последовательным, а происходит через коллбэки.

Параллельность

Тип вычислений, когда множество операций выполняются параллельно. Современные процессоры способствуют этому, так как имеют множество ядер.

Синхронизация

Cнятие блокировки потока должно происходить на том же потоке, которым она и была захвачена. Иначе поток зависнет навсегда.

Java Memory Model

Модель памяти Java. Часть семантики языка. Описывает поведение потоков в среде исполнения Java. Определяет правила и гарантии, касающиеся порядка доступа к переменным из различных потоков. JMM определена в терминах actions (действий) - чтение и запись в переменные, lock и unlock мониторов, запуск start и join потоков.

• Однопоточные программы исполняются псевдопоследовательно. Это значит: в реальности процессор может выполнять несколько операций за такт, заодно изменив их порядок, однако все зависимости по данным остаются, так что поведение не отличается от последовательного.

• Нет невесть откуда взявшихся значений. Чтение любой переменной (кроме не-volatile long и double, для которых это правило может не выполняться) выдаст либо значение по умолчанию (ноль), либо что-то, записанное туда другой командой.

• Остальные события выполняются по порядку, если связаны отношением строгого частичного порядка “выполняется прежде” (happens before).

Heap (Куча)

Часть памяти которая хранит фактические объекты (экземпляры классов).

• создание нового объекта происходит в куче.

• любой объект, созданный в куче, имеет глобальный доступ и на него могут ссылаться из любой части приложения.

• куча это огромный объем памяти по сравнению со стеком.

• если память кучи заполнена вызывается OutOfMemoryError.

• максимальный размер кучи не определен заранее - зависит от работающей JVM машины. обычно это вся доступная оперативная память (~500 MB).

Stack

Стековая память отвечает за хранение ссылок на объекты кучи и за хранение типов значений (примитивные типы в Java) которые содержат само значение а не ссылку на объект кучи.

• работает по схеме LIFO (last in - first out).

• стековая память в Java выделяется для каждого потока. каждый раз когда поток создается и запускается он имеет свою собственную стековую память и не может получить доступ к стековой памяти другого потока.

• из-за простоты распределения памяти стековая память работает намного быстрее кучи.

• если память стека полностью занята - вызывается StackOverflowError.

• максимальный размер стека не определен заранее - зависит от работающей JVM машины. обычно это ~1 MB.

Разница между Stack и Heap?

• куча используется всеми частями приложения в то время как стек используется только одним потоком исполнения программы.

• всякий раз, когда создается объект, он всегда хранится в куче, а в памяти стека содержится ссылка на него. Память стека содержит только локальные переменные примитивных типов и ссылки на объекты в куче.

• объекты в куче доступны с любой точки программы, в то время как стековая память не может быть доступна для других потоков.

• управление памятью в стеке осуществляется по схеме LIFO.

• стековая память существует лишь какое-то время работы программы, а память в куче живет с самого начала до конца работы программы.

• если память стека полностью занята, то Java Runtime бросает java.lang.StackOverflowError, а если память кучи заполнена, то бросается исключение java.lang.OutOfMemoryError: Java Heap Space.

• размер памяти стека намного меньше памяти в куче. Из-за простоты распределения памяти (LIFO), стековая память работает намного быстрее кучи.

Happens before

JMM задает частичный порядок, который называется happens-before, для всех действий (actions) в рамках программы. Для того, чтобы гарантировать, что поток, который выполняет действие B может видеть результат действия A (в не зависимости A и B выполняются в одном потоке или нет), необходимо, чтобы существовала happens-before связь между A и B. Если такой связи нет, то JVM может выполнять действия A и B в произвольном порядке. И никаких гарантий видимости не накладывается. Отношение happens-before является отношением частичного порядка между двумя операциями. Если одна операция происходит-до другой, то ее результат видим и упорядочен для другой.

Runnable

Интерфейс содержащий метод run в котором выполняется код потока.

Method Area

В области Method area хранится скомпилированный код для каждой функции. Когда поток начинает выполнять функцию, он получает инструкции из этой области. По сути, она представляет собой кулинарную книгу рецептов, где подробно описано, как что приготовить.

ThreadPool

Механизм управления потоками, который позволяет эффективно использовать ресурсы и управлять многопоточностью, повторно использовать потоки для выполнения задач и избегать избыточного создания и уничтожения потоков.

Повторное использование потоков. В ThreadPool потоки создаются один раз и повторно используются для выполнения множества задач, что снижает затраты на создание и уничтожение потоков.

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

Очередь задач. Задачи, которые нужно выполнить, помещаются в очередь задач. Потоки из пула извлекают задачи из этой очереди и выполняют их.

fun main() {
    // Создание пула с фиксированным количеством потоков
    val executorService = Executors.newFixedThreadPool(3)
    
    // Отправка задач в пул
    repeat(10) { taskId ->
        executorService.submit {
            println("Task $taskId is running on ${Thread.currentThread().name}")
            Thread.sleep(2000) // Имитация работы
        }
    }
    
    // Завершение работы пула и ожидание завершения всех задач
    executorService.shutdown()
}

FixedThreadPool

Создает фиксированное количество потоков. Если все потоки заняты, новые задачи помещаются в очередь до тех пор, пока не освободится один из потоков.

CachedThreadPool

Создает потоки по мере необходимости, а не фиксированное количество. Использует пул потоков, который может расширяться и сжиматься в зависимости от нагрузки.

SingleThreadExecutor

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

ScheduledThreadPool

Позволяет выполнять задачи с задержкой или с периодическим интервалом. Полезен для задач, которые должны запускаться через определенные интервалы времени.

ExecutorService

ExecutorService исполняет асинхронный код в одном или нескольких потоках. Создание экземпляра ExecutorService делается либо вручную через конкретные реализации (ScheduledThreadPoolExecutor или ThreadPoolExecutor), но проще будет использовать фабрики класса Executors.

ExecutorService service = Executors.newFixedThreadPool(2); // пул с 2 потоками
ExecutorService service = Executors.newCachedThreadPool(); // кэширующий пул потоков, который создает потоки по мере необходимости, но переиспользует неактивные потоки

Executor

Executor — интерфейс, который может выполнять подтвержденные задачи. Предоставляет возможность избежать вникания в механику выполнения задачи и деталей использования выполняемого потока. Executor обычно используется для явного создания потоков.

ScheduledExecutorService

Иногда требуется выполнение кода асихронно и периодически или требуется выполнить код через некоторое время, тогда на помощь приходит ScheduledExecutorService. Он позволяет поставить код выполняться в одном или нескольких потоках и сконфигурировать интервал или время, на которое выполненение будет отложено. Интервалом может быть время между двумя последовательными запусками или время между окончанием одного выполнения и началом другого.

ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
service.schedule(new Runnable() { ... }, 5, TimeUnit.SECONDS);

ThreadPoolExecutor

Реализация ExecutorService. Выполняет переданную задачу (Callable или Runnable), используя одну из внутренних доступных нитей из пула. Пул потоков содержит в себе ThreadPoolExecutor, который может содержать изменяющееся число потоков. Число нитей в пуле задается с помощью corePoolSize и maximumPoolSize.

ScheduledThreadPoolExecutor

Является наследником ThreadPoolExecutor, определенного в пакете java.util.concurrent. Как видно из его названия, этот класс полезен, когда мы хотим запланировать многократное выполнение задач или запуск после заданной задержки на какое-то время в будущем. Он создает пул потоков фиксированного размера. Поэтому, когда он инициируется, ему необходимо указать corePoolSize.

Future

Интерфейс описывает API для работы с задачами, результат которых мы планируем получить в будущем: методы получения результата, методы проверки статуса.

Callable task = () -> {
    return "Hello, World!";
};
FutureTask<String> future = new FutureTask<>(task);
new Thread(future).start();
synchronized

Если метод или блок кода помечен ключевым словом synchronized это значит, что блок может выполняться только одним потоком одновременно. Позволяет заблокировать доступ к методу или части кода, если его уже использует другой поток. Если метод синхронизирован то вся логика внутри него выполняется одним потоком одновременно. Если один поток зашел внутрь блока кода, который помечен словом synchronized, он моментально захватывает мьютекс объекта, и все другие потоки, которые попытаются зайти в этот же блок или метод вынуждены ждать, пока предыдущий поток не завершит свою работу и не освободит монитор.
• если в
synchronized блоке возникнет исключение - блокировка будет снята.

public synchronized void doSomething() {
    // целый синхронизированный метод
}
private Object obj = new Object();

public void doSomething() {
    //...логика, доступная для всех потоков
    synchronized (obj) {
        // логика, которая одновременно доступна только для одного потока
    }
}
volatile

Переменная volatile всегда будет атомарно читаться и записываться. Даже если это 64-битные double или long. JVM не будет помещать ее в кэш. Так что ситуация, когда 10 потоков работают со своими локальными копиями исключена. Не используется кэш (область памяти в которой JVM может сохранять локальную копию переменной, чтобы уменьшить время обращения к ней) при обращении к полю. Для volatile JVM гарантирует синхронизацию для операций чтения/записи, но не гарантирует для операций изменения значения переменной (инкремент/декремент) - использовать атомарный тип

atomic

В процессе работы многопоточного приложения разные потоки могут кэшировать значения переменных. Возможна ситуация, когда один поток изменил значение переменной, а второй не увидел этого изменения, потому что работал со своей, кэшированной копией переменной. В Java операции чтения и записи полей всех типов, кроме long и double, являются атомарными. Если в одном потоке изменить значение перменной, а в другом попытаться ее считать, то получим либо ее старое закэшированное значение, либо новое, без промежуточных вариантов. С long и double это не работает из-за кроссплатформенности, это тяжеловесные типы, весят по 64 бита. В 32-битных платформах не реализована атомарность чтения и записи 64-битных переменных, они читаются и записываются в две операции. Сначала в переменную записываются первые 32 бита, потом еще 32. В это время и может возникнуть ошибка, которая решается ключевым словом volatile

Операция называется атомарной, если её можно безопасно выполнять при параллельных вычислениях в нескольких потоках, не используя при этом ни блокировок, ни synchronized. Пример: инкремент, декримент.

mutex

«MUTual EXclusion» — «взаимное исключение». Специальный объект для синхронизации потоков. Прикреплен к каждому объекту в Java. Задача мьютекса — обеспечить такой механизм, чтобы доступ к объекту в определенное время был только у одного потока. Если Поток-1 захватил мьютекс объекта А, остальные потоки не получат к нему доступ, чтобы что-то в нем менять. До тех пор, пока мьютекс объекта А не освободится, остальные потоки будут вынуждены ждать. Мьютекс - мячик, говорит тот у кого он в руках. Он уже встроен в класс Object а значит, есть у каждого объекта в Java.

Монитор

Надстройка над мьютексом. Невидимый кусок кода, обеспечивающий защитный механизм объекта от других потоков. Монитор в Java выражен с помощью слова synchronized.

Semaphore

Семафор - средство для синхронизации доступа к какому-то ресурсу. При создании механизма синхронизации он использует счетчик (сколько потоков одновременно могут получать доступ к общему ресурсу).

permits - начальное и максимальное значение счетчика. То есть то, сколько потоков одновременно могут иметь доступ к общему ресурсу.

fair - для установления порядка, в котором потоки будут получать доступ. Если fair = true, доступ предоставляется ожидающим потокам в том порядке, в котором они его запрашивали. Если же он равен false, порядок будет определять планировщик потоков.

acquire - запрашивает разрешение на доступ к ресурсу у семафора. Если счетчик > 0, разрешение предоставляется, а счетчик уменьшается на 1.

release - «освобождает» выданное ранее разрешение и возвращает его в счетчик (увеличивает счетчик разрешений семафора на 1).

Semaphore(int permits)
Semaphore(int permits, boolean fair)

Semaphore semaphore = new Semaphore(2);
semaphore.acquire();
semaphore.release();
ThreadLocal

Класс который хранит локальные переменные для потоков. Эти переменные изолированы между разными потоками и могут быть доступны только своему собственному потоку.

• Изоляция данных между потоками.

• Управление сессиями для подключения к базе данных.

• Хранение транзакционной информации потока.

ThreadLocal<String> local = new ThreadLocal<>();
ReentrantLock

Класс, который предоставляет механизм блокировки потоков с возможностью повторного захвата. Это улучшенная альтернатива традиционным синхронизированным методам (использующим ключевое слово synchronized) и предоставляет больше гибкости в управлении потоками и синхронизации.

• Позволяет одному и тому же потоку захватывать блокировку несколько раз. Это удобно, если поток уже владеет блокировкой и нужно выполнить дополнительный захват, например, при вызове вложенного метода, который также требует блокировки.

ReentrantLock lock = new ReentrantLock();

void method() {
    lock.lock();
    try {
        // Внутренний код
        nestedMethod();
    } finally {
        lock.unlock();
    }
}

void nestedMethod() {
    lock.lock(); // Можно захватить блокировку снова
    try {
        // Внутренний код
    } finally {
        lock.unlock(); // Нужно разблокировать все захваты
    }
}

• Предоставляет методы для явного захвата и освобождения блокировки, что позволяет лучше контролировать процесс синхронизации.

ReentrantLock lock = new ReentrantLock();

lock.lock(); // Захват блокировки
try {
    // Критическая секция
} finally {
    lock.unlock(); // Освобождение блокировки
}

• Поддерживает создание условных переменных через newCondition, что позволяет реализовывать более сложные сценарии синхронизации, такие как ожидание и уведомление.

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

void waitMethod() throws InterruptedException {
    lock.lock();
    try {
        condition.await(); // Ожидание сигнала
    } finally {
        lock.unlock();
    }
}

void signalMethod() {
    lock.lock();
    try {
        condition.signal(); // Отправка сигнала
    } finally {
        lock.unlock();
    }
}

• Позволяет захватывать блокировку с таймаутом через методы tryLock и tryLock(long, TimeUnit)

ReentrantLock lock = new ReentrantLock();
boolean acquired = lock.tryLock(1000, TimeUnit.MILLISECONDS); // Попытка захвата блокировки с таймаутом
if (acquired) {
    try {
        // Критическая секция
    } finally {
        lock.unlock();
    }
}

• Может быть создан с флагом справедливости. Если блокировка создаётся с true для параметра fair, то она будет соблюдать порядок захвата: потоки будут захватывать блокировку в порядке их запроса.

ReentrantLock fairLock = new ReentrantLock(true); // Справедливая блокировка
deadlock

Взаимная блокировка. Возникает только в случае неверного порядка синхронизации. Разрешить deadlock нельзя.
Чтобы предотвратить блокировку:

• использовать метод Thread.join с максимальным временем, которое вы хотите ждать, чтобы поток завершился.

• использовать упорядочивание блокировок.

• избегать ненужных блокировок.

Race condition

Гонка потоков. Если из разных потоков менять одно и тоже поле класса, то это небезопасно и нельзя гарантировать, что они будут изменены в том же порядке, что и задумано. Это называется состояние гонки.

Concurrent Collections

Потокобезопасные коллекции при итерировании не бросают ConcurrentModificationException.

synchronized List

Потокобезопасный List.

List<String> synchronizedList = Collections.synchronizedList(list);

synchronized Set

Потокобезопасный Set.

Set<String> synchronizedSet = Collections.synchronizedSet(new HashSet<>());

CopyOnWriteArrayList

Потокобезопасный ArrayList.

• операции add set remove созданиют новую копию внутреннего массива (копируются только ссылки на объекты)

List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArraySet

Имплементация интерфейса Set использующая за основу CopyOnWriteArrayList. В отличии от CopyOnWriteArrayList дополнительных методов нет.

Set<String> set = new CopyOnWriteArraySet<>();
Java Concurrent. Вопросы на собесе
  1. Что такое Stack и Heap?
  1. Какие есть способы синхронизации потоков?
  1. Как synchronized работает?
  1. В чем разница между мьютексом, монитором и семафором?
  1. Какие есть способы предотвратить deadlock?
  1. Какие есть способы разрешить Race Condition?
  1. Как обеспечить потокобезопасность при работе с коллекциями?
  1. Что такое ThreadPool и зачем они нужны?
  1. Чему ThreadPool лучше цикла потоков new Thread?

    Создание и уничтожение потоков для каждой задачи может быть затратным по времени и ресурсам, особенно если задача выполняется быстро и часто. Каждый новый поток требует выделения ресурсов и времени на инициализацию.

    При большом количестве задач может быть создано слишком много потоков, что приведет к большому количеству переключений контекста и снижению производительности.

    Обычные потоки: Не предоставляют встроенного механизма для управления задачами, что может привести к проблемам, если задачи поступают быстрее, чем могут быть выполнены.

    Обычные потоки: Требуется вручную управлять жизненным циклом потоков и обработкой исключений, что может усложнить код.

    Обычные потоки: Обработка исключений и ошибок в каждом потоке может быть трудоемкой и запутанной.

  1. На каком объекте происходит синхронизация статического метода с ключевым словом synchronized?

    На объекте класса