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

Хорошо, это наша повестка дня на сегодня.

  • Понимание параллелизма потоков.
  • Многопоточность с Thread
  • Многопоточность с Runnable
  • Многопоточность с ExecutorService

Понимание параллелизма потоков

Я хочу начать с определений

поток — наименьшая единица выполнения, которую может запланировать операционная система.

процесс — группа связанных потоков, которые выполняются в одной среде.

системный поток — поток, созданный JVM и работающий в фоновом режиме приложения.

Например, поток сборщика мусора Java является системным потоком и работает в фоновом режиме вместе с нашими потоками для очистки неиспользуемых объектов.

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

поток демона — поток, который не будет препятствовать выходу JVM после завершения программы. Если поток создает другой поток, созданный поток будет демоном, если им является только исходный поток (в основном он наследует это свойство).

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

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

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

Один из способов создания дополнительных потоков — расширение класса Thread.

Сколько всего потоков (определенных пользователем) было выполнено в приведенной выше программе?

Ответ — 3. Первый — наш основной метод, это поток по умолчанию в нашей программе. А затем мы запускаем еще два потока внутри этого потока.

Поэтому нам нужно расширить класс Thread и переопределить метод run(). Затем нам нужно создать объект этого класса и просто начать в потоке с методом start(). Что бы мы ни поместили внутрь run(), метод будет выполняться как отдельный определяемый пользователем поток.

Каков результат приведенного выше кода? Мы не знаем. Один из возможных выходов

Starting out threads..
The end.
one: 1
one: 2
two: 1
one: 3
two: 2
two: 3

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

Другой способ добиться многопоточности — реализовать интерфейс Runnable.

Один из возможных выходов:

Starting out threads..
The end.
one: 1
two: 1
one: 2
two: 2
one: 3
two: 3

У нас также есть три потока здесь.

На этот раз наш класс реализует наш интерфейс Runnable и переопределяет метод запуска. Для запуска нам по-прежнему нужно передать экземпляр нашего класса объекту Thread и использовать метод запуска.

В чем разница между этими двумя вариантами?

  • Если вам нужно определить собственные правила для многопоточности, такие как приоритет и т. д., хорошей идеей будет расширение класса Thread.
  • При использовании Runnable вы можете позволить своему классу расширять некоторые другие классы, но это не будет работать при расширении класса Thread (java не поддерживает множественное наследование).
  • Реализация Runnable позволяет использовать класс во многих классах Concurrency API.

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

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

Методы ExecutorService (неполный список):

void execute(команда Runnable)Этот метод унаследован от интерфейса Executor. По сути, он может выполнять заданную задачу в новом потоке или в потоке из пула. Аргумент представляет собой функциональный интерфейс Runnable, поэтому он может принимать аргумент как лямбда-выражение.

Future‹?› submit(Runnable task)Этот метод работает аналогично методу execute, но возвращает Future, который можно использовать для мониторинга задачи.

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

boolean isShutdown() возвращает true, если исполнитель вызвал метод выключения (перестает принимать новые потоки). Но это не означает, что все потоки завершены.

boolean isTerminated() вернет true, если исполнитель не выполнил ни одной задачи (все задачи завершены).

List‹Runnable› shutdownNow() пытается остановить все выполняемые задачи. Возвращает список Runnable — ожидающих задач.

Новый однопоточный исполнитель

Один из возможных выходов:

Start
From custom thread one: 0
From custom thread one: 1
From custom thread one: 2
End
From custom thread two: 0
From custom thread two: 1
From custom thread two: 2
  • Реализация newSingleThreadExecutor может выполнять/отправлять только один поток за раз.
  • Темы, представленные им, гарантированно будут в порядке. Таким образом, когда один отправленный поток завершит выполнение, начнется следующий.
  • В приведенном выше примере создаются два пользовательских потока, но они не параллельны. Второй отправленный поток будет ждать первого, а затем начнет выполнение. Вы можете видеть, как End может появиться в середине двух исполнений, потому что это оператор из основного потока и, конечно же, он не зависит от отправленных двух.

Новый фиксированный пул потоков

Один из возможных выходов:

Start
From custom thread one: 0
End
From custom thread two: 0
From custom thread one: 1
From custom thread two: 1
From custom thread one: 2
From custom thread two: 2
  • newFixedThreadPool создает пул потоков на основе своего аргумента. В нашем примере мы создали пул с 5 потоками. Исполнитель будет использовать эти 5 потоков для параллельной отправки потоков. Если количество отправленных потоков превышает размер пула, исполнитель будет ждать следующего доступного потока.
  • если бы мы создали пул размером 1, он вел бы себя как исполнитель newSingleThreadExecutor.

Новый кэшированный пул потоков

service = Executors.newCachedThreadPool();
  • он создает пул потоков, который создает новые потоки по мере необходимости. И он повторно использует доступные старые потоки. Таким образом, в принципе, нет ограничений на количество потоков для этого исполнителя, и он будет повторно использовать старые потоки, как только они закончат выполнение.

Вы можете использовать ScheduledExecutorService(interface)с реализациями newSingleThreadScheduledExecutor и newScheduledThreadPool для планирования или запуска ваших потоков через определенные промежутки времени. .

На сегодня все, хорошего дня!

Ссылки и ресурсы, использованные в этой статье:
1. Книга OCP.
2. Официальная документация по Java.

Please take my Java Course for video lectures.
This article is part of the series of articles to learn Java programming language from Tech Lead Academy:
Introduction to programming 
OS, File, and File System
Working with terminal 
Welcome to Java Programming Language
Variables and Primitives in Java
Convert String to numeric data type
Input from the terminal in Java
Methods with Java
Java Math Operators and special operators
Conditional branching in Java
Switch statement in Java
Ternary operator in Java
Enum in Java
String class and its methods in Java
Loops in Java
Access modifiers in Java
Static keyword in Java
The final keyword in Java
Class and Object in Java
Object-Oriented Programming in Java
OOP: Encapsulation in Java
OOP: Inheritance in Java
OOP: Abstraction in Java
OOP: Polymorphism in Java
The method Overriding vs Overloading in Java
Array in Java
Data Structures with Java
Collection framework in Java
ArrayList in Java
Set in Java
Map in Java
Date and Time in Java
Exception in Java
How to work with files in Java
Design Patterns
Generics in Java
Multithreading in java
Annotations in Java
Reflection in Java
Reflection & Annotations - The Powerful Combination
Run terminal commands from Java
Lambda in Java
Unit Testing in Java
Big O Notation for coding interviews
Top Java coding interview questions for SDET