Skip to content
Gallery
Theory for java developer
Share
Explore
Java

icon picker
Multithreading

Процесс vs Поток Процесс - изолирован от других, не делят общую память. Более общая сущность содержит потоки. Поток - делят общую память между собой.
Процессы и потоки связаны друг с другом, но при этом имеют существенные различия.
Процесс — экземпляр программы во время выполнения, независимый объект, которому выделены системные ресурсы (например, процессорное время и память). Каждый процесс выполняется в отдельном адресном пространстве: один процесс не может получить доступ к переменным и структурам данных другого. Если процесс хочет получить доступ к чужим ресурсам, необходимо использовать межпроцессное взаимодействие. Это могут быть конвейеры, файлы, каналы связи между компьютерами и многое другое.
Поток использует то же самое пространства стека, что и процесс, а множество потоков совместно используют данные своих состояний. Как правило, каждый поток может работать (читать и писать) с одной и той же областью памяти, в отличие от процессов, которые не могут просто так получить доступ к памяти другого процесса. У каждого потока есть собственные регистры и собственный стек, но другие потоки могут их использовать.
Поток — определенный способ выполнения процесса. Когда один поток изменяет ресурс процесса, это изменение сразу же становится видно другим потокам этого процесса.

Многопоточность

// если мьютекс занят любой другой поток - спит
while (obj.getMutex().isBusy()) {
Thread.sleep(1);
}
// пометить мьютекс обьекта как занятый
obj.getMutex().isBusy() = true;
// логика которая должен делать только 1 поток
obj.someImportantMethod();
// освободить мьютекс обьекта
obj.getMutex().isBusy() = false;
Мьютекс — это специальный механизм для синхронизации потоков. Он есть у каждого объекта в java. Задача мьютекса — сделать так чтобы доступ к объекту в определенное время был только у одного потока. Возможны только два состояния — «свободен» и «занят», состояниями нельзя управлять напрямую
Монитор — это дополнительная «надстройка» над мьютексом, «невидимый» для программиста кусок кода. В блоке кода, который помечен словом synchronized, происходит захват мьютекса объекта. Защитный механизм создает именно монитор!
Компилятор преобразует слово synchronized в несколько специальных кусков кода(пример рядом) это и есть монитор.
Семафор — это средство для синхронизации доступа к какому-то ресурсу, при создании механизма синхронизации он использует счетчик который указывает сколько потоков одновременно могут получать доступ к общему ресурсу. (мьютекс = двоичный семафор - счетчик с 1 разрешением) Semaphore(int permits) \\ (int permits, boolean fair)
permits — сколько потоков одновременно могут иметь доступ к общему ресурсу. fair = true, доступ предоставляется ожидающим потокам в том порядке, в котором они его запрашивали.
Метод acquire() - запрашивает разрешение на доступ к ресурсу у семафора. Если счетчик > 0, разрешение предоставляется, а счетчик уменьшается на 1.
Метод release() - «освобождает» выданное ранее разрешение и возвращает его в счетчик (увеличивает счетчик разрешений семафора на 1).
synchronized - Можно применять как модификатор метода, и как самостоятельный оператор с блоком кода. Выполняет код при захваченном мониторе объекта. В виде оператора объект указывается явно. В виде модификатора нестатического метода используется this, статического – .class текущего класса.
Производительность: Atomic переменные лучше чем synchronized, потому что они используют параллелизм и аппаратные механизмы Compare-And-Swap. А synchronized механизм блокировки, поэтому она медленнее.
volatile - Java-машина не будет помещать переменную в кэш и вынуждает потоки отключить оптимизацию доступа и использовать единственный экземпляр переменной. Если переменная примитивного типа – этого будет достаточно для обеспечения потокобезопасности. Если же переменная является ссылкой на объект – синхронизировано будет исключительно значение этой ссылки. Значит, между ними существует отношение happens-before. Это значит, что существует гарантия, что произошедшее в памяти до записи будет видно после чтения. То есть будут успешно прочитаны значения, записанные в другие переменные.
CountDownLatch - дословно «Защелка», – примитив синхронизации из стандартной библиотеки Java. Он останавливает пришедшие потоки, пока внутренний счетчик не достигнет 0. Чтобы поставить поток на ожидание, нужно вызвать из него метод await().
Начальное значение счетчика задается параметром конструктора, затем уменьшается на 1 методом countDown(). Узнать текущее значение можно с помощью getCount(). Изменение значения счетчика никак не связано с потоками, его можно вызывать откуда угодно.
CyclicBarrier – барьер для потоков, который ломается при достижении критической массы ожидающих. Поток также встает на ожидание методом await(). Ожидающие потоки называются parties, их лимит также устанавливается в конструкторе.
Технически, parties барьера и count латча – одно и то же, await барьера – это await+countDown от CountDownLatch. В барьере тоже доступна информация о текущем состоянии барьера (методы isBroken, getParties и getNumberWaiting).
Помимо этого, CyclicBarrier дает две дополнительных возможности. Во-первых, в конструктор кроме parties можно передать коллбэк с действием, которое выполнится в момент прорыва барьера. Во-вторых, этот примитив переиспользуется: метод reset() насильно прорывает текущий барьер и устанавливает новый.
Есть специальный пакет с классами Atomic в java.

Пакет Concurrency

ReentrantLock - решает те же задачи, что и блок synchronized. Поток висит на вызове метода lock() в ожидании своей очереди занять этот объект. Владеть локом, как и находиться внутри блока synchronized может только один поток одновременно. unlock(), подобно выходу из блока синхронизации, освобождает объект-монитор для других потоков.
В отличие от блока синхронизации, ReentrantLock дает расширенный интерфейс для получения информации о состоянии блокировки. Методы лока позволяют еще до блокировки узнать, занят ли он сейчас, сколько потоков ждут его в очереди, сколько раз подряд текущий поток завладел им.
Шире и возможные режимы блокировки. Кроме обычного ожидающего lock(), вариант tryLock() с параметром ожидает своей очереди только заданное время, а без параметра – вообще не ждет, а только захватывает свободный лок.
Еще одно отличие – свойство fair. Лок с этим свойством обеспечивает «справедливость» очереди: пришедший раньше поток захватывает объект раньше. Блок synchronized не дает никаких гарантий порядка.

ExecutorService

Данное средство служит альтернативой классу Thread, предназначенному для управления потоками. Исполнители выполняют задачи асинхронно и обычно используют пул потоков, так что нам не надо создавать их вручную. Все потоки из пула будут использованы повторно после выполнения задачи, а значит, мы можем создать в приложении столько задач, сколько хотим, используя один исполнитель.
Executor пакета java.util.concurrent не требуется прибегать к низкоуровневой поточной функциональности класса Thread, достаточно создать объект типа ExecutorService с нужными свойствами и передать ему на исполнение задачу типа Callable. Впоследствии можно легко просмотреть результат выполнения этой задачи с помощью объекта Future.
Интерфейс ExecutorService расширяет свойства Executor, дополняя его методами управления исполнением и контроля. Так в интерфейс ExecutorService включен метод shutdown(), позволяющий останавливать все потоки исполнения, находящиеся под управлением экземпляра ExecutorService. Также в интерфейсе ExecutorService определяются методы, которые запускают потоки исполнения FutureTask, возвращающие результаты и позволяющие определять статус остановки.

Callable vs Runnable

Кроме Runnable, исполнители могут принимать другой вид задач, который называется Callable. Callable - это также функциональный интерфейс, но, в отличие от Runnable, он может возвращать значение.
Callable-задачи также могут быть переданы исполнителям. Но как тогда получить результат, который они возвращают? Поскольку метод submit() не ждет завершения задачи, исполнитель не может вернуть результат задачи напрямую. Вместо этого исполнитель возвращает специальный объект Future, у которого мы можем запросить результат задачи.
Future future = ExecutorService.submit(Операция может быть Runnable или Callable).
isDone() - проверить что операция завершена.
get() - блокирует выполнение до получения результата.
InvokeAll() - Исполнители могут принимать список задач на выполнение с помощью метода invokeAll(), который принимает коллекцию callable-задач и возвращает список из Future.
InvokeAny() - Другой способ отдать на выполнение несколько задач — метод invokeAny(). Он работает немного по-другому: вместо возврата Future он блокирует поток до того, как завершится хоть одна задача, и возвращает ее результат.

Future vs CompletableFuture 

// A task that sleeps for a second, then returns 1
public static class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return 1;
}
}
public static void main(String[]) args) throws Exception {
ExecutorService exec = Executors.newSingleThreadExecutor();
Future<Integer> f = exec.submit(new MyCallable());
log(f.isDone()); // False
log(f.get()); // Waits until the task is done, then prints 1
}
CompletableFuture появился в Java 8. Это класс-реализация старого интерфейса Future, а значит всё сказанное выше справедливо и для него. Вдобавок к этому, CompletableFuture реализует работу с отложенными результатами посредством колбэков. Метод thenApply() регистрирует код обработки значения, который будет автоматически вызван позже, когда это значение появится.
Future - Java 5 (2004). Представляет пока еще не вычисленный результат. Когда породившая его асинхронная операция заканчивается, он заполняется значением.

// A task that sleeps for a second, then returns 1
public static class MyCallable implements Supplier<Integer> {
@Override
public Integer get() throws Exception {
Thread.sleep(1000);
return 1;
}
}
public static class PlusOne implements Funciton<Integer, Integer> {
@Ovveride
public Integer apply(Integer x) {
return x + 1;
}
}
public static void main(String[]) args) throws Exception {
ExecutorService e = Executors.newSingleThreadExecutor();
CompletableFuture<Integer> f =
CompletableFuture.supplyAsync(new MyCallable(), e);
log(f.isDone()); // False
CompletableFuture<Integer> f2 = f.thenApply(new PlusOne());
log(f.get()); // Waits until the task is done, then prints 2
}
CompletableFuture - Java 8 (2014). Эволюция обычного Future, позволяет объединять в цепь задач(как стримы или Mono - почти также обрабатываются исключения). Вы можете использовать их чтобы сказать “Сделай эту задачу, когда она завершится сделай другую, используя результат предыдущей". Реализует работу с отложенными результатами посредством колбэков. Вы можете сделать что-то с результатом операции без блокировки потока.

Fork/Join Framework

Выпуск 35. Как работает ForkJoinPool.
Дробит сложные операции на простейшие и так рекурсивно до тех пор пока конечная операция не станет элементарной.
Насколько я понимаю разница тут в следующем:
ExecutorService имеет общую очередь задач и некоторое количество потоков, которые забирают по очереди таски и выполняют их.
ForkJoinPool имеет некоторое количество потоков, но при этом еще имеет очередь тасков для каждого потока. Поток в процессе работы может дробить задачу на несколько тасков, одну он добавляет к себе в очередь а другую выполняет и это может рекурсивно повторяться. Если другой поток опустошил свою очередь, то он может взять задачи у другого потока, с конца очереди. Описанный механизм называется work-stealing. Это и является ключевым моментом различающим данные thread-pool'ы.
Рекомендуется использовать ForkJoinPool, если у вас есть набор задач, которые рекурсивно повторяются. Это позволяет вовсю использовать work-stealing. Если же задачи никак не разбиваются в процессе работы, то и выгоды никакой не получите, хотя и минусов от использование тоже не встретите.

Механизм CAS(Compare and Swap)

public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if(compareAndSet(current, next))
return next;
}
}

Механизм микропроцессорного взаимодействия который позволяет на основании оптимистичной блокировки осуществлять синхронизацию между потоками. Применяется в АТОМИК классах. Алгоритм:
Место в памяти для работы (M)
Существующее ожидаемое значение (A) переменной
Новое значение (B), которое необходимо установить
Операция CAS атомарно обновляет значение в M до B, но только если существующее значение в M совпадает с A, в противном случае никаких действий не предпринимается.
Когда несколько потоков пытаются обновить одно и то же значение через CAS, один из них выигрывает и обновляет значение. Зацикленность постоянна пока ожидаемое значение не совпадает.
Want to print your doc?
This is not the way.
Try clicking the ⋯ next to your doc name or using a keyboard shortcut (
CtrlP
) instead.