Многопоточность.JAVA Flashcards
Что такое многопоточность?
Многопоточность в Java - это возможность программы выполнять несколько потоков одновременно, потоки позволяют программе выполнять несколько задач параллельно, что приводит к увеличению производительности.
Concurrency - Принцип построения программы, при котором несколько блоков кода могут выполняться одновременно.
Многопоточность позволяет выполнять код параллельно с другим кодом.
Многопоточность может работать с любым количеством ядер.
Чтобы создать дополнительный поток в Java, можно либо наследовать класс Thread, либо реализовать интерфейс Runnable. Для запуска потока необходимо вызвать метод start(), который создаст новый поток и выполнит метод run() в этом потоке.
В Джаве могопоточность представлена в виде виртуальной параллельности.
В одноядерном процессоре: Context Switch
В многоядерном процессоре: Context Switch + Parallelism
Синхронно - последовательно
Асинхронно - параллельно
Что такое и чем процесс отличается от потока? (4)
Основное отличие между процессом и потоком заключается в том, что процесс является изолированным экземпляром любой программы с собственной памятью и контекстом исполнения, а поток является легковесным процессом, который работает в рамках основного процесса и имеет общую память с другими потоками. Процессы обычно более надежны и изолированы друг от друга, но потоки более эффективны и могут легче совместно использовать ресурсы компьютера.
Как создать поток?
1) Создать класс extends Thread.
2) Передать в new Thread() объект Runnable. Можно использовать анонимный класс или лямбда-выражение.
Процесс - это экземпляр программы, с выделенными ей ресурсами: памятью, процессорным временем и т.д.
Поток - набор команд, выполняющийся на центральном процессоре. Потоки находятся внутри процесса.
Процесс гораздо тяжелее потока, его запуск и содержание обходится гораздо дороже.
Каждый процесс выполняется в своем собственном адресном пространстве, тогда как потоки разделяют общие ресурсы процесса, такие как память, файловые дескрипторы и сетевые соединения.
Потоки позволяют параллельно выполнять несколько задач внутри одного процесса, что повышает производительность и улучшает отзывчивость приложения.
Каждый процесс имеет свои собственные обработчики сигналов, а каждый поток использует обработчики сигналов процесса.
Каждый процесс имеет свой набор открытых файлов, а каждый поток использует общие файловые дескрипторы процесса.
Каждый процесс имеет свой собственный стек вызовов, а каждый поток использует общую область стека процесса.
JVM, испоняющая байткод - процесс, а GC в ней - поток
Каждый процесс имеет свой уникальный идентификатор (PID), а каждый поток имеет свой уникальный идентификатор (TID).
Чем Thread отличается от Runnable?
Thread и Runnable - это два способа создания и запуска потоков в Java.
- Класс || Интерфейс(множественное наследование)
- Методы || 1 абстрактный метод
- Свой стек вызовов || стек вызовов потока
- Можно запустить поток через метод .start() || Должны быть переданы в конструктор Тред и запущены через старт
- Можно остановить через стоп || можно остановить только через флаг прерывания
- Есть методы для синхронизации || нет методов
- Нельзя использовать в тред пуле || можно использовать в тред пуле
Runnable – это функциональный интерфейс с единственным методом public void run(). Экземпляр Runnable не является потоком, а просто формулирует задачу, которую можно выполнить в отдельном потоке.
Помимо того, что Runnable помогает разрешить проблему множественного наследования, несомненный плюс от его использования состоит в том, что он позволяет логически отделить логику выполнения задачи от непосредственного управления потоком.
Thread implements Runnable – это класс, содержащий методы для запуска и управления состоянием потока, экземпляр которого мы можем создать и запустить в отдельном потоке, переопределив метод run().
Класс Thread позволяет наследоваться от него и переопределять методы, в то время как интерфейс Runnable не предоставляет такой возможности.
Каждый экземпляр класса Thread имеет свой собственный стек вызовов, а объекты, реализующие интерфейс Runnable, используют стек вызовов своего потока.
Класс Thread позволяет запускать новый поток напрямую, вызывая метод start(), а объекты, реализующие интерфейс Runnable, должны быть переданы в конструктор Thread и запущены методом start().
Класс Thread позволяет остановить поток вызовом метода stop(), который может привести к непредсказуемому поведению, а объекты, реализующие интерфейс Runnable, могут быть остановлены только с помощью флага прерывания.
Класс Thread имеет ряд методов, связанных с управлением потоком, таких как sleep(), yield(), join() и interrupt(), которые не доступны в объектах, реализующих интерфейс Runnable.
Класс Thread имеет ряд методов, связанных с синхронизацией потоков, таких как wait(), notify() и notifyAll(), которые не доступны в объектах, реализующих интерфейс Runnable.
Использование класса Thread обычно требует больше ресурсов, чем использование объектов, реализующих интерфейс Runnable.
Объекты, реализующие интерфейс Runnable, могут использоваться для выполнения задач в пуле потоков (ThreadPool), что позволяет управлять ресурсами процессора более эффективно. Класс Thread не предоставляет такой возможности.
Каждый экземпляр класса Thread имеет свой уникальный идентификатор (TID), а объекты, реализующие интерфейс Runnable, не имеют собственного идентификатора и используют идентификатор своего потока.
Когда нужно использовать Thread, а когда Runnable?
Thread и Runnable используются для создания параллельных процессов в Java приложениях.
Используй Thread если
-Нужны методы синхронизации
-Если нужно задать Имя потока
-Если нужно выполнить задачи в основном потоке приложения
Используй Runnable если
-Если нужно выполнить задачу в фоне
-Если нужно использовать лямбду для создания потока
-Если нужно сформировать класс и унаследоваться от еще одного класса
Runnable является интерфейсом, который содержит единственный метод run(), который не возвращает значения. Для использования Runnable нужно создать объект, реализующий этот интерфейс, и передать его в Thread.
Runnable - это функциональный интерфейс, что позволяет создавать потоки более гибко и использовать лямбда-выражения для определения задач.
Thread предоставляет некоторые дополнительные методы для работы с потоками, такие как управление приоритетом и приостановкой.
Если вы планируете запустить несколько потоков, то лучше использовать Runnable, так как этот интерфейс может быть реализован несколькими классами, а Thread - только одним.
Если вам нужно запустить поток, который наследует от другого класса, в этом случае можно использовать Thread, т.к. он является классом и может быть наследован.
Thread позволяет задать имя потока, что может быть удобно при отладке многопоточных приложений.
Используйте класс Thread, если вам нужно выполнить какие-то задачи в основном потоке приложения, например, для управления графическим интерфейсом пользователя (GUI).
Используйте класс Runnable, если вам нужно выполнить отдельную задачу в фоновом потоке приложения, не влияя на основной поток выполнения.
что такое Мьютекс (Mutex)
Мьютекс (Mutex) в Java - это механизм синхронизации доступа к общим ресурсам между несколькими потоками. Простыми словами, мьютекс - это объект, который используется для блокировки доступа к общим ресурсам одним потоком в то время, когда другой поток уже выполняет работу с этим ресурсом.
Что такое монитор? Как монитор реализован в java?
Монитор это средство обеспечения контроля за доступом к ресурсам. У каждого объекта есть свой монитор.
Монитор встроен в класс Object и имеется у каждого объекта или класса. Он имеет 2 состояния: занят и свободен.
Монитор можно представить как обычное boolean поле. Следовательно если поле true, нет никаких препятствий чтобы поток выполнил synchronized метод или блок. И выставляет его сразу в false пока он не выйдет из данного метода или блока. Если другой поток пытается в это время вызвать synchronized метод или блок то это невозможно, и поток будет ждать его освобождения.
В Java монитор реализован с помощью ключевого слова synchronized, причем ни методы ни блоки не имеют монитора, используется монитор this в методе неявно, и в блоке явно. в static class используется монитор класса.
Методы wait(), notify() и notifyAll() также относятся к монитору в Java. Они используются для управления потоками, которые ожидают изменений состояния объекта и могут быть вызваны только при захваченном мониторе.
Что такое синхронизация? Какие способы синхронизации существуют в java?
Синхронизация - это процесс координации работы нескольких потоков
Требуется для обеспечения правильного выполнения операций над общими ресурсами.
Цель синхронизации состоит в том, чтобы избежать состояний гонки (race conditions), которые могут возникать при параллельном доступе к общим ресурсам и могут приводить к непредсказуемому поведению программы.
В Java существует несколько способов синхронизации потоков:
1.synchronized
2.Методы wait() и notify() / notifyAll().
3.join()
4.Блокировки (Lock) и условные переменные (Condition).
5.Атомарные операции.
6.Коллекции из пакета java.util.concurrent.
Ключевое слово synchronized. Оно позволяет защитить критические участки кода от параллельного доступа нескольких потоков. Код, помеченный как synchronized, может быть выполнен только одним потоком одновременно, остальные потоки будут ожидать, пока монитор не будет освобожден.
Методы wait() и notify() / notifyAll(). Они позволяют потокам синхронизироваться на условиях, связанных с состоянием общих ресурсов. wait() приостанавливает выполнение потока, пока другой поток не оповестит его о наступлении определенного условия с помощью методов notify() или notifyAll().
Системная синхронизация с использованием join().
Метод join(), вызванный у экземпляра класса Thread, позволяет текущему потоку остановиться до того момента, как поток, связанный с этим экземпляром, закончит работу.
Блокировки (Lock) и условные переменные (Condition). Блокировки являются более гибким механизмом синхронизации, чем ключевое слово synchronized, так как они позволяют более точно управлять захватом и освобождением мониторов. Условные переменные позволяют потокам синхронизироваться на условиях, связанных с состоянием общих ресурсов, аналогично методам wait() и notify().
Атомарные операции. Они позволяют выполнять операции над переменными без возможности параллельного доступа к ним. В Java существует несколько классов, которые обеспечивают атомарность операций, такие как AtomicInteger и AtomicLong.
Коллекции из пакета java.util.concurrent. Эти коллекции обеспечивают безопасный доступ к общим ресурсам из нескольких потоков. Например, класс ConcurrentHashMap обеспечивает безопасную работу с хеш-таблицами из нескольких потоков.
Выбор конкретного способа синхронизации зависит от требований к производительности, критичности к сбоям и других факторов, связанных с конкретным приложением.
В каких состояниях может находиться поток?
New - объект класса Thread создан, но еще не запущен. Он еще не является потоком выполнения и естественно не выполняется.
Runnable - поток готов к выполнению, но планировщик еще не выбрал его.
Running – поток выполняется.
Waiting/blocked/sleeping - поток блокирован или поток ждет окончания работы другого потока.
Dead - поток завершен. Будет выброшено исключение при попытке вызвать метод start() для dead потока.
public enum State (У класса Thread есть внутренний класс State - состояние, а также метод public State getState().)
1) NEW - создан, не стартовал.
2) RUNNABLE - работает или ждет в очереди на процессорное время.
3) BLOCKED - ждет мьютекс (synchronized{} или lock.lock()). Дождавшись, перейдет в RUNNABLE.
4) WAITING - при вызове thread.join(), object.wait(), condition.await(), когда ждет notify(All)/signal(All).
5) TIMED_WAITING - как WAITING, но также пробуждается, если прошло заданное время. Методы Thread.sleep, object.wait(time), thread.join(time), lock.tryLock(time), condition.await(time).
6) TERMINATED - когда закончился метод run() или случилось RuntimeException и вышло за пределы метода run().
}
Что обозначает ключевое слово volatile? Почему операции над volatile
переменными не атомарны?
volatile (изменчивый) – означает, что переменная может быть изменена.
volatile применимо только к примитивным типам данным и к ссылкам.
Переменная, объявленная как volatile, хранится в основной памяти (main memory), а не в кэше потока (thread cache). Это означает, что каждый поток, обращающийся к volatile переменной, получает доступ к единственной копии переменной в основной памяти, а не к локальной копии, хранящейся в кэше потока., гарантирует когерентность кэшей.
Таким образом обновления значения переменной будут видны всем потокам, которые работают с этой переменной, и что потоки будут видеть актуальное значение переменной в каждый момент времени.
Однако, volatile не делает операцию присваивания атомарной.Там три операции: считать значение, присвоить новое, записать в память.
Операция, которая делает одно чтение/запись – атомарна.
Compare-And-Swap в Atomic
Java Atomic CAS (Compare-And-Swap) является одним из механизмов синхронизации для многопоточной работы с общей памятью. Этот механизм позволяет обеспечивать атомарность операций чтения-изменения-записи (RMW) в разделяемой памяти.
Операция CAS состоит из трех операций:
Сравнение (compare): сравнивает текущее значение переменной с ожидаемым значением.
Обновление (update): если текущее значение переменной равно ожидаемому, то значение переменной обновляется новым значением.
Возврат (return): возвращается булево значение, которое указывает на успешность выполнения операции.
Если два или более потоков одновременно пытаются выполнить операцию CAS на одной и той же переменной, только один из них успешно выполнит операцию, а остальные будут возвращены с неудачным результатом.
Java Atomic CAS обычно используется для обновления значения переменной без блокировки всей переменной, что увеличивает производительность при работе с разделяемой памятью в многопоточных приложениях.
Что такое Атомарные операции в Java.
Примером атомарной операции в Java может служить инкрементация (увеличение значения на 1) переменной типа AtomicInteger.
public class Example { private static AtomicInteger counter = new AtomicInteger(0); public static void main(String[] args) { int oldValue = counter.getAndIncrement(); System.out.println("Old value = " + oldValue + ", new value = " + counter.get()); } }
В этом примере переменная counter типа AtomicInteger инициализируется значением 0. Затем в методе main() происходит инкрементация переменной с помощью метода getAndIncrement(), который возвращает предыдущее значение переменной и увеличивает его на 1. Таким образом, в переменной counter после выполнения этой операции сохранится новое значение, равное предыдущему значению плюс 1.
Метод getAndIncrement() является атомарной операцией, что означает, что он выполняется целиком и никакой другой поток не может одновременно изменять значение переменной counter. Это гарантирует корректность работы программы в многопоточной среде.
Атомарная операция в Java - это операция, которая выполняется как единое целое, без прерываний и не может быть разделена на более мелкие операции. Такие операции гарантируют, что в случае многопоточности другие потоки не могут изменять данные во время выполнения атомарной операции. Примерами атомарных операций в Java являются чтение и запись примитивных типов данных (кроме long и double) и операции из класса java.util.concurrent.atomic.
В Java атомарные операции поддерживаются через классы-обертки, которые обеспечивают атомарный доступ к переменным типа byte, short, int, long, boolean и ссылочным типам. Например, классы AtomicBoolean, AtomicInteger, AtomicLong и т.д.
Некоторые из атомарных операций, которые можно выполнить на этих классах-обертках, включают в себя:
get() - чтение значения переменной
set() - установка значения переменной
getAndSet() - чтение и установка значения переменной за одну операцию
compareAndSet() - сравнение текущего значения переменной с ожидаемым значением и, если они совпадают, установка нового значения переменной
incrementAndGet() - инкрементирование значения переменной и возврат нового значения
Эти операции являются атомарными, поскольку они выполняются за один шаг без возможности прерывания и гарантируют правильность результатов даже в многопоточной среде. Однако, если вы хотите выполнить несколько атомарных операций одновременно, вы можете использовать блокировки или другие механизмы синхронизации, чтобы обеспечить атомарность выполнения группы операций.
как устроен инкремент в атомик?
Атомарность операции инкремента в классе AtomicLong достигается за счет использования механизма CAS (Compare-And-Swap).
Этот механизм позволяет сравнить текущее значение переменной с заданным значением и, если они совпадают, заменить его на новое значение.
Если же значения не совпадают, операция не выполняется и повторяется заново.
- Создать экземпляр класса AtomicLong и проинициализировать начальным значением.
- Вызвать метод incrementAndGet() для выполнения операции инкремента.
- Значение переменной atomicLong увеличится на 1 и вернется новое значение.
В Java атомарные операции выполняются без блокировки, что позволяет нескольким потокам одновременно выполнять операции над одним и тем же объектом. Один из примеров атомарных операций - это операция инкремента (увеличения значения на 1).
Атомарный инкремент в Java реализуется с помощью класса AtomicLong, который предоставляет атомарную версию операции инкремента над переменной типа long.
Для выполнения атомарного инкремента необходимо сделать следующее:
Создать экземпляр класса AtomicLong и проинициализировать его начальным значением:
Вызвать метод incrementAndGet() для выполнения операции инкремента. Этот метод инкрементирует значение переменной и возвращает ее новое значение:
В результате выполнения этой операции, значение переменной atomicLong увеличится на 1 и вернется новое значение.
Атомарность операции инкремента в классе AtomicLong достигается за счет использования механизма CAS (Compare-And-Swap). Этот механизм позволяет сравнить текущее значение переменной с заданным значением и, если они совпадают, заменить его на новое значение. Если же значения не совпадают, операция не выполняется и повторяется заново.
Таким образом, при использовании атомарных операций, в том числе и атомарного инкремента, мы можем обеспечить безопасное выполнение операций в многопоточной среде, избежав проблем, связанных с гонками данных (race conditions) и блокировками.
Для чего нужны Atomic типы данных? Чем отличаются от volatile?
Чтобы безопасно выполнять операции при параллельных вычислениях в нескольких потоках не используя при этом ни блокировок, ни синхронизацию synchronized.
volatile принуждает использовать единственный экземпляр переменной, но не гарантирует атомарность. Например, операция count++ не станет атомарной просто потому, что count объявлена volatile.
Могут быть использованы как улучшеные volatile переменные
C другой стороны, class AtomicInteger предоставляет атомарный метод для выполнения таких комплексных операций атомарно, например getAndIncrement() – атомарная замена оператора инкремента, его можно использовать, чтобы атомарно увеличить текущее значение на один.
Похожим образом сконструированы атомарные версии и для других типов данных.
Операция называется атомарной, если её можно безопасно выполнять при параллельных вычислениях в нескольких потоках, не используя при этом ни блокировок, ни синхронизацию synchronized.
Чем Runnable отличается от Callable?
Оба интерфейса предназначены для представления задачи, которая может выполняться несколькими потоками однако, между ними есть несколько отличий:
RUNNABLE || CALLABLE
не возвращает || возвращает
не обрабатывает || обрабатывает исключения
не поддерживает || дженерики поддерживает
Возвращаемое значение: метод run() интерфейса Runnable не возвращает никакого значения, тогда как Callable.call() возвращает объект Future, который может содержать результат вычислений;
Обработка исключений: метод run() не может выбрасывать проверяемые исключения, а метод call() может. В случае выброса исключения, вызывающий поток должен его обработать.
Поддержка Generic: интерфейс Callable параметризован типом возвращаемого значения, что позволяет использовать Generic.
Способ использования: Runnable используется в основном с Executor и Thread, в то время как Callable используется в основном с ExecutorService.
Область применения: Callable может быть использован для выполнения задач, которые требуют выполнения длительных вычислений, в то время как Runnable может использоваться для выполнения задач, которые не возвращают результат или не требуют обработки исключений.
Если нужно возвращать результат и бросать исключения - Использовать Callable
Если не нужно чтобы возвращал результат то Runnable
Как работает Thread.join()? Для чего он нужен?
Метод join() в Java используется для ожидания завершения выполнения потока, на котором он был вызван.
Когда поток вызывает метод join() для другого потока, он ожидает, пока поток, на котором был вызван join(), завершит свое выполнение. Это означает, что выполнение текущего потока приостанавливается до тех пор, пока поток, на котором был вызван join(), не завершится.
Пример использования метода join():
Thread thread1 = new Thread(() -> { // выполнение кода потока 1 }); Thread thread2 = new Thread(() -> { // выполнение кода потока 2 }); // запуск потоков thread1.start(); thread2.start(); // ожидание завершения потока 1 try { thread1.join(); } catch (InterruptedException e) { e.printStackTrace(); }
// выполнение кода после завершения потока 1
В приведенном выше примере поток 1 запускается в отдельном потоке, а затем вызывается метод join(), чтобы поток 2 не начал свое выполнение, пока поток 1 не завершится. После завершения потока 1 выполнение кода продолжится.
Таким образом, метод join() позволяет контролировать порядок выполнения потоков и гарантирует, что определенный поток завершится перед продолжением выполнения других потоков или основного потока.
Thread.join() позволяет синхронизировать потоки, чтобы главный поток (или другой поток) не продолжал свою работу до тех пор, пока не завершится указанный поток.
void join()
void join(long millis) - с временем ожидания
void join(long millis, int nanos)
Применение: при распараллелили вычисления, вам надо дождаться результатов, чтобы собрать их в кучу и продолжить выполнение.