2.85M
Category: programmingprogramming

Многопоточность. Процесс и поток

1.

Многопоточность
astondevs.ru

2.

Процесс и поток
1.
Процесс - это совокупность кода и данных, разделяющих общее виртуальное адресное
пространство.
2.
Поток - это одна единица исполнения кода. Каждый поток последовательно выполняет
инструкции процесса, которому он принадлежит, параллельно с другими потоками
этого процесса.

3.

Создание потока
В языке Java поток представляется в виде объекта-потомка
класса Thread. Этот класс инкапсулирует стандартные
механизмы работы с потоком.
Есть 2 основных способа создания потока:
1. Имплементировать Runnable и переопределить его
абстрактный метод run() именно он и будет запущен.
2. Наследоваться от класса Thread и переопределяет его
метод run().
* Можно передать Runnable/Callable в один из пул потоков, но
там так или иначе будет использоваться Thread.

4.

Завершение потоков
Нельзя принудительно завершать потоки, потому что, мб утечка данных в случае io, или
неправильное изменение объекта.
Вместо принудительного завершения потока применяется схема, в которой каждый поток сам
ответственен за своё завершение. Поток может остановиться либо тогда, когда он закончит
выполнение метода run(), (main() — для главного потока) либо по сигналу из другого потока.
Причем как реагировать на такой сигнал — дело, опять же, самого потока.
Для реализации этого подхода класс Thread содержит в себе скрытое булево поле, которое
называется флагом прерывания. Установить этот флаг можно вызвав метод interrupt() потока.
Проверить же, установлен ли этот флаг, можно двумя способами:
1. bool isInterrupted() // возвращает состояние флага прерывания и оставляет этот флаг нетронутым
2. static bool Thread.interrupted() // возвращает состояние флага и сбрасывает его
* Когда нить вызывает методы: sleep()/join()/wait(), в этих методах происходит скрытая
проверка – флага isInterrupted. Если этот флаг выставлен, то методы выбрасывают
исключение InterruptedException.

5.

join()/join(timeout)
Join - в Java предусмотрен механизм, позволяющий одному потоку ждать завершения
выполнения другого. Для этого используется метод join(). Например, чтобы главный поток
подождал завершения побочного потока sleepy, необходимо выполнить инструкцию
sleepy.join() в главном потоке.

6.

Thread.sleep(timeout)
Данный статический метод останавливает текущую нить (в которой sleep был вызван) на
timeout миллисекунд. Нить при этом переходит в состояние TIMED_WAITING.
Метод может завершиться раньше, если был установлен флаг isInterrupted.
Если вызывается в синхронизированном методе, блокировка не отдается.
Несмотря на то, что метод sleep() может принимать в качестве времени ожидания
наносекунды, не стоит принимать это всерьез. Во многих системах время ожидания все равно
округляется до миллисекунд а то и до их десятков.

7.

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

8.

Жизненный цикл потока
У класса Thread есть
внутренний класс State, а
также метод public State
getState().

9.

Приоритетность
В реальных задачах важность работы разных нитей может сильно различаться. Для контроля
этого процесса был придуман приоритет работы. У каждой нити есть такой приоритет – это
число от 1 до 10. Если приоритет не задан, то нить получает приоритет 5 — средний.
Приоритет нити не сильно влияет на ее работу, а носит скорее рекомендательный характер.
Если есть несколько спящих нитей, которые нужно запустить, то Java-машина сначала
запустит нить с более высоким приоритетом.

10.

Потоки демоны
Java позволяет запустить нить в «режиме демона» (daemon). Работа такой нити ничем не
отличается от других, но если в программе все обычные нити завершили работу, но остались
только нити-демоны — Java-машина завершит программу.

11.

UncaughtExceptionHandler
UncaughtExceptionHandler позволяет установить обработчик непроверяемых (unchecked)
исключений для потока.
Существует три способа установки обработчика необрабатываемых ошибок.
1. setUncaughtExceptionHandler() класса Thread.
2. Создать объект ThreadGroup и изменять поведение всех потоков в группе переопределив
метод uncaughtException().
3. Установить поведение по умолчанию для всех потоков вызвав статический метод
setDefaultUncaughtExceptionHandler() класса Thread (всех включая main).

12.

ThreadLocal
ThreadLocal переменные отличаются от обычных переменных тем, что у каждого потока свой
собственный, индивидуально инициализируемый экземпляр переменной, доступ к которой
он получает через методы get() или set().
Концептуально ThreadLocal можно рассматривать, как HashMap у которой ключ это поток.

13.

Синхронизация
Java поддерживает несколько потоков для выполнения. Это может привести к тому, что два
или более потока получат доступ к одному и тому же полю или объекту, что может
привести к “неправильному” изменению состояния.
Синхронизация — это процесс, который позволяет выполнять все параллельные потоки в
программе синхронно, т.е. один за другим по очереди.
Синхронизация позволяет избежать ошибок согласованности памяти, вызванные из-за
непоследовательного доступа к общей памяти.
Синхронизация стоит дорого, поэтому не стоит ее использовать везде и подходить к ней
стоит обоснованно!

14.

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

15.

Мютекс/монитор
Мютекс – это специальный объект для синхронизации нитей/процессов. Он может
принимать два состояния – занят и свободен.
Когда нить хочет монопольно владеть некоторым объектом, она помечает его мютекс
занятым. Такой мютекс прикреплен к каждому объекту в Java. Работать с мютексом в Java
можно посредством монитора.
Монитор – это специальный механизм (кусок кода) – надстройка над мютексом, который
обеспечивает правильную работу с ним. Ведь мало пометить, что объект – занят, надо еще
обеспечить, чтобы другие нити не пробовали воспользоваться занятым объектом.
В Java монитор реализован с помощью ключевого слова synchronized. Когда ты пишешь
блок synchronized, то компилятор Java заменяет его тремя кусками кода:
1. В начале блока synchronized добавляется код, который отмечает мютекс как занятый.
2. В конце блока synchronized добавляется код, который отмечает мютекс как свободный.
3. Перед блоком synchronized добавляется код, который смотрит, если мютекс занят – то
нить должна ждать его освобождения.

16.

Мютекс/монитор
На самом деле логика там другая и сложнее, но это уже детали.
По сути, монитор в Java выражен с помощью слова synchronized. Весь код, который
появился вместо слова synchronized в последнем примере, — это и есть монитор.

17.

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

18.

Синхронизация на уровне класса
Синхронизация на уровне класса, для 1-го и
того же ClassLoader’a.
Это означает, что если во время выполнения
программы имеется 100 экземпляров класса,
то только один поток в это время сможет
выполнить demoMethod().
Не зависит от обычных объектов,
синхронизируемся на уровне объектов типа
Class

19.

Вопрос на 1 балл
Можно ли одновременно вызвать статический и нестатический
синхронизированные методы из разных потоков?

20.

Вопрос на 1 балл
Освободит ли поток мютекс, если во время работы метода вылетит
исключение?

21.

Самоизучение
1. Wait()/notify()/notifyAll
2. Взаимные блокировки потоков (dead lock/ live lock/ starvation/
race condition) и способы их избегания

22.

JMM
Модель памяти Java (Java Memory Model, JMM) описывает поведение потоков в среде
исполнения Java. Это часть семантики языка Java, набор правил, описывающий
выполнение многопоточных программ и правил, по которым потоки могут
взаимодействовать друг с другом посредством основной памяти.
Формально модель памяти определяет набор действий межпоточного взаимодействия, а
также определяет отношение между этими действиями -happens-before.

23.

Happens before
happens-before определяет правила работы потоков в многопоточной среде и выделяет
ряд правил:
1. Освобождение монитора (unlock) happens-before захват того же монитора (lock);
2. Выход из synchronized блока/метода happens-before вход
в synchronized блок/метод на том же мониторе;
3. Запись volatile поля happens-before чтение того же самого volatile поля;
4. Завершение метода run() экземпляра класса Thread happens-before выход из
метода join() или возвращение false методом isAlive() экземпляром того же потока;
5. Вызов метода start() экземпляра класса Thread happens-before начало
метода run() экземпляра того же потока;
6. Завершение конструктора happens-before начало метода finalize() этого класса;
7. Вызов метода interrupt() на потоке happens-before обнаружению потоком факта, что
данный метод был вызван либо путем выбрасывания исключения InterruptedException,
либо с помощью методов isInterrupted() или interrupted().

24.

Happens before
Важны нюансы о happens-before:
1. Связь happens-before транзитивна, т.е. если X happens-before Y, а Y happens-before Z,
то X happens-before Z.
2. Как освобождение/захват монитора, так и запись/чтение в volatile переменную
связаны отношением happens-before, только если операции проводятся над одним и
тем же экземпляром объекта.
3. В отношении happens-before участвуют только два потока, о поведении остальных
потоков ничего сказать нельзя, пока в каждом из них не наступит отношение happensbefore с другим потоком.
4. Еще в отношении happens-before есть очень большой дополнительный бонус: данное
отношение дает не только видимость volatile полей или результатов операций
защищенных монитором или локом, но и видимость вообще всего-всего, что делалось
до события hapens-before.

25.

JMM
Можно выделить несколько основных областей, имеющих отношение к модели памяти:
1.
Видимость (visibility)
2.
Переупорядочивание (Reordering)

26.

Видимость
Один поток может в какой-то момент временно сохранить значение некоторых полей не в
основную память, а в регистры или локальный кэш процессора, таким образом второй
поток, выполняемый на другом процессоре, читая из основной памяти, может не увидеть
последних изменений поля и наоборот.
К вопросу видимости имеют отношение следующие ключевые слова языка Java:
1. synchronized - При входе в synchronized метод или блок поток обновляет содержимое
локальной памяти, а при выходе из synchronized метода или блока поток записывает
изменения, сделанные в локальной памяти, в главную. Следует из правил happensbefore
2. volatile - запись и чтение таких переменных производится в/из основную память,
минуя локальную.
3. final - после того как объект был корректно создан, любой поток может видеть
значения его final полей без дополнительной синхронизации. «Корректно создан»
означает, что ссылка на создающийся объект не должна использоваться до тех пор,
пока не завершился конструктор объекта. Данная семантика позволяет создавать
immutable классы и безопасно использовать их в многопоточной среде.

27.

Переупорядочивание
Для увеличения производительности процессор/компилятор могут переставлять местами некоторые
инструкции/операции. Вернее, с точки зрения потока, наблюдающего за выполнением операций в
другом потоке, операции могут быть выполнены не в том порядке, в котором они идут в исходном
коде.
Тот же эффект может наблюдаться, когда один поток кладет результаты первой операции в регистр
или локальный кэш, а результат второй операции попадает непосредственно в основную память.
Тогда второй поток, обращаясь к основной памяти может сначала увидеть результат второй
операции, и только потом первой, когда все регистры или кэши синхронизируются с основной
памятью.
Вопрос reordering также регулируется набором правил для отношения happens-before и у этих правил
есть следствие, касающееся порядка операций, используемое на практике: операции чтения и
записи volatile переменных не могут быть переупорядочены с операциями чтения и записи
других volatile и не-volatile переменных. Это следствие делает возможным
использование volatile переменной как флага, сигнализирующем об окончании какого-либо действия.
В остальном правила, касающиеся порядка выполнения операций, гарантируют упорядоченность
операций для конкретного набора случаев (см 23 слайд)

28.

JMM

29.

30.

Atomic
Пакет java.util.concurrent.atomic содержит классы для выполнения атомарных операций.
Операция называется атомарной, если её можно безопасно выполнять при параллельных
вычислениях в нескольких потоках.
С точки зрения программиста операции инкремента (i++, ++i) и декремента (i--, --i) выглядят
наглядно и компактно. Но, с точки зрения JVM данные операции не являются атомарными,
поскольку требуют выполнения нескольких действительно атомарных операции: чтение текущего
значения, выполнение инкремента/декремента и запись полученного результата.
Для безопасной реализации таких операций в многопоточной среде, можно использовать 2
подхода:
1. Пессимистичный – synchronized/Lock API
2. Оптимистичный – Atomic классы.

31.

AtomicLong
Рассмотрим принцип действия механизма оптимистичного подхода на примере атомарного
класса AtomicLong с методом getAndAdd, который позволяет увеличить текущее значение.
В этом классе переменная value объявлена с модификатором volatile, т.е. её значение могут
поменять разные потоки одновременно. Модификатор volatile гарантирует выполнение
отношения happens-before, что ведет к тому, что измененное значение этой переменной увидят
все потоки.
При попытке увеличить AtomicLong
используется метод compareAndSet,
который позволяет выполнить эту
операцию оптимистично.

32.

CAS and FAA
Оптимистичная реализация атомиков достигается за счет двух алгоритмов:
1. CAS – compare and swap
2. FAA – fetch and add
Темы для самоизучения!

33.

Lock API
Lock Api предоставляет схожий функционал с обычной синхронизацией посредством
synchronized, но более расширенный:
1. A synchronized block is fully contained within a method – we can
have Lock API's lock() and unlock() operation in separate methods
2. A synchronized block doesn't support the fairness, any thread can acquire the lock once released,
no preference can be specified. We can achieve fairness within the Lock APIs by specifying
the fairness property. It makes sure that longest waiting thread is given access to the lock
3. A thread gets blocked if it can't get an access to the synchronized block. The Lock API
provides tryLock() method. The thread acquires lock only if it's available and not held by any other
thread. This reduces blocking time of thread waiting for the lock
4. A thread which is in “waiting” state to acquire the access to synchronized block, can't be
interrupted. The Lock API provides a method lockInterruptibly() which can be used to interrupt the
thread when it's waiting for the lock

34.

ReadWriteLock

35.

Synchronizers
Semaphore - объект синхронизации, ограничивающий количество потоков, которые могут
«войти» в заданный участок кода;
CountDownLatch - объект синхронизации, заблокировать потоки до тех пор, пока другие
не закончат;
CyclicBarrier - объект синхронизации типа «барьер», блокирующий выполнение
определенного кода для заданного количества потоков;
Exchanger - объект синхронизации, позволяющий провести обмен данными между двумя
потоками;
Phaser - объект синхронизации типа «барьер», но в отличие от CyclicBarrier,
предоставляет больше гибкости, за счет добавления такого понятия, как фаза. В каждой
фазе может учавствовать различное количество потоков.

36.

Пулы потоков

37.

Виды пулов потоков

38.

Коллекции
ConcurrentHashMap
ConcurrentMap;
коллекция типа HashMap, реализующая интерфейс
CopyOnWriteArrayList
коллекция типа ArrayList с алгоритмом CopyOnWrite;
CopyOnWriteArraySet
CopyOnWriteArrayList;
реализация интерфейса Set, использующая за основу
ConcurrentNavigableMap
расширяет интерфейс NavigableMap;
ConcurrentSkipListMap
аналог коллекции TreeMap с сортировкой данных по ключу и с
поддержкой многопоточности;
ConcurrentSkipListSet
ConcurrentSkipListMap.
реализация интерфейса Set, выполненная на основе класса

39.

Очереди
Неблокирующие очереди

ConcurrentLinkedQueue

ConcurrentLinkedDeque
Блокирующие очереди

ArrayBlockingQueue

LinkedBlockingQueue

LinkedBlockingDeque

SynchronousQueue

LinkedTransferQueue

DelayQueue

PriorityBlockingQueue
English     Русский Rules