Thread’ом Java не испортишь: Часть I — потоки
Многопоточность в Java была заложена с самых первых дней. Поэтому давайте кратко ознакомимся с тем, про что это — многопоточность. Возьмём за точку отсчёта официальный урок от Oracle: «Lesson: The «Hello World!» Application». Код нашего Hello World приложения немного изменим на следующий:
args — это массив входных параметров, передаваемых при запуске программы. Сохраним данный код в файл с именем, которое совпадает с именем класса и с расширением .java . Скомпилируем при помощи утилиты javac: javac HelloWorldApp.java После этого вызовем наш код с каким-нибудь параметром, например, Roger: java HelloWorldApp Roger У нашего кода сейчас есть серьёзный изъян. Если не передать никакой аргумент (т.е. выполнить просто java HelloWorldApp), мы получим ошибку:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0 at HelloWorldApp.main(HelloWorldApp.java:3)
Возникло исключение (т.е. ошибка) в thread (в потоке) с именем main . Получается, в Java есть какие-то потоки? Отсюда начинается наш путь.
Java и потоки
Чтобы разобраться, что такое поток, надо понять, как происходит запуск Java приложения. Давайте изменим наш код следующим образом:
Теперь давайте скомпилируем это снова при помощи javac. Далее для удобства запустим наш Java код в отдельном окне. В Windows это можно сделать так: start java HelloWorldApp . Теперь при помощи утилиты jps посмотрим, какую информацию нам сообщит Java: Первое число — это PID или Process ID, идентификатор процесса. Что такое процесс?
Процесс — это совокупность кода и данных, разделяющих общее виртуальное адресное пространство.
При помощи процессов выполнение разных программ изолировано друг от друга: каждое приложение использует свою область памяти, не мешая другим программам. Более подробно советую ознакомиться в статье: «https://habr.com/post/164487/». Процесс не может существовать без потоков, поэтому если существует процесс, в нём существует хотя бы один поток. Как же это происходит в Java? Когда мы запускаем Java программу, ее выполнение начинается с метода main . Мы как бы входим в программу, поэтому этот особый метод main называется точкой входа, или «entry point». Метод main всегда должен быть public static void , чтобы виртуальная машина Java (JVM) смогла начать выполнение нашей программы. Подробнее см. «Why is the Java main method static?». Получается, что java launcher (java.exe или javaw.exe) — это простое приложение (simple C application): оно загружает различные DLL, которые на самом деле являются JVM. Java launcher выполняет определённый набор Java Native Interface (JNI) вызовов. JNI — это механизм, соединяющий мир виртуальной машины Java и мир C++. Получается, что launcher — это не JVM, а её загрузчик. Он знает, какие правильные команды нужно выполнить, чтобы запустилась JVM. Знает, как организовать всё необходимое окружение при помощи JNI вызовов. В эту организацию окружения входит и создание главного потока, который обычно называется main . Чтобы нагляднее рассмотреть, какие живут потоки в java процессе, используем программу jvisualvm, которая входит в поставку JDK. Зная pid процесса, мы можем открыть данные сразу по нему: jvisualvm —openpid айдипроцесса Интересно, что каждый поток имеет свою обособленную область в памяти, выделенной для процесса. Эту структуру памяти называют стеком. Стек состоит из фрэймов. Фрэйм — это точка вызова метода, execution point. Также фрэйм может быть представлен как StackTraceElement (см. Java API для StackTraceElement). Подробнее про память, выделяемую каждому потоку, можно прочитать тут. Если посмотреть на Java API и поискать там слово Thread, мы увидим, что есть класс java.lang.Thread. Именно этот класс представляет в Java поток, и с ним нам и предстоит работать.
java.lang.Thread
- Не вызван метод Runtime.exit
- Все НЕ демон-потоки завершили свою работу (как без ошибок, так и с выбрасыванием исключений)
public static void main(String []args)
Группы позволяют упорядочить управление потоками и вести их учёт. Помимо групп, у потоков есть свой обработчик исключений. Взглянем на пример:
public static void main(String []args) < Thread th = Thread.currentThread(); th.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() < @Override public void uncaughtException(Thread t, Throwable e) < System.out.println("Возникла ошибка: " + e.getMessage()); >>); System.out.println(2/0); >
Деление на ноль вызовет ошибку, которая будет перехвачена обработчиком. Если обработчик не указывать самому, отработает реализация обработчика по умолчанию, которая будет в StdError выводить стэк ошибки. Подробнее можно прочитать в обзоре http://pro-java.ru/java-dlya-opytnyx/obrabotchik-neperexvachennyx-isklyuchenij-java/». Кроме того, у потока есть приоритет. Подробнее про приоритеты можно прочитать в статье «Java Thread Priority in Multithreading».
Создание потока
Как и сказано в документации, у нас 2 способа создать поток. Первый — создать своего наследника. Например:
public class HelloWorld < public static class MyThread extends Thread < @Override public void run() < System.out.println("Hello, World!"); >> public static void main(String []args) < Thread thread = new MyThread(); thread.start(); >>
Как видим, запуск задачи выполняется в методе run , а запуск потока в методе start . Не стоит их путать, т.к. если мы запустим метод run напрямую, никакой новый поток не будет запущен. Именно метод start просит JVM создать новый поток. Вариант с потомком от Thread плох уже тем, что мы в иерархию классов включаем Thread. Второй минус — мы начинаем нарушать принцип «Единственной ответственности» SOLID, т.к. наш класс становится одновременно ответственным и за управление потоком и за некоторую задачу, которая должна выполняться в этом потоке. Как же правильно? Ответ находится в том самом методе run , который мы переопределяем:
Здесь target — это некоторый java.lang.Runnable , который мы можем передать для Thread при создании экземпляра класса. Поэтому, мы можем сделать так:
public class HelloWorld < public static void main(String []args)< Runnable task = new Runnable() < public void run() < System.out.println("Hello, World!"); >>; Thread thread = new Thread(task); thread.start(); > >
А ещё Runnable является функциональным интерфейсом начиная с Java 1.8. Это позволяет писать код задач для потоков ещё красивее:
public static void main(String []args) < Runnable task = () ->< System.out.println("Hello, World!"); >; Thread thread = new Thread(task); thread.start(); >
Итого
Итак, надеюсь, из сего повестования понятно, что такое поток, как они существуют и какие базовые операции с ними можно выполнять. В следующей части стоит разобраться, как потоки взаимодействуют друг с другом и какой у них жизненный цикл. #Viacheslav
Java Threads
Typically, we can define threads as a subprocess with lightweight with the smallest unit of processes and also has separate paths of execution. These threads use shared memory but they act independently hence if there is an exception in threads that do not affect the working of other threads despite them sharing the same memory.
Threads in a Shared Memory Environment in OS
As we can observe in, the above diagram a thread runs inside the process and there will be context-based switching between threads there can be multiple processes running in OS, and each process again can have multiple threads running simultaneously. The Multithreading concept is popularly applied in games, animation…etc.
The Concept Of Multitasking
To help users Operating System accommodates users the privilege of multitasking, where users can perform multiple actions simultaneously on the machine. This Multitasking can be enabled in two ways:
1. Process-Based Multitasking (Multiprocessing)
In this type of Multitasking, processes are heavyweight and each process was allocated by a separate memory area. And as the process is heavyweight the cost of communication between processes is high and it takes a long time for switching between processes as it involves actions such as loading, saving in registers, updating maps, lists, etc.
2. Thread-Based Multitasking
As we discussed above Threads are provided with lightweight nature and share the same address space, and the cost of communication between threads is also low.
Why Threads are used?
Now, we can understand why threads are being used as they had the advantage of being lightweight and can provide communication between multiple threads at a Low Cost contributing to effective multi-tasking within a shared memory environment.
Life Cycle Of Thread
There are different states Thread transfers into during its lifetime, let us know about those states in the following lines: in its lifetime, a thread undergoes the following states, namely:
- New State
- Active State
- Waiting/Blocked State
- Timed Waiting State
- Terminated State
We can see the working of different states in a Thread in the above Diagram, let us know in detail each and every state:
1. New State
By default, a Thread will be in a new state, in this state, code has not yet been run and the execution process is not yet initiated.
2. Active State
A Thread that is a new state by default gets transferred to Active state when it invokes the start() method, his Active state contains two sub-states namely:
- Runnable State: In This State, The Thread is ready to run at any given time and it’s the job of the Thread Scheduler to provide the thread time for the runnable state preserved threads. A program that has obtained Multithreading shares slices of time intervals which are shared between threads hence, these threads run for some short span of time and wait in the runnable state to get their schedules slice of a time interval.
- Running State: When The Thread Receives CPU allocated by Thread Scheduler, it transfers from the “Runnable” state to the “Running” state. and after the expiry of its given time slice session, it again moves back to the “Runnable” state and waits for its next time slice.
3. Waiting/Blocked State
If a Thread is inactive but on a temporary time, then either it is a waiting or blocked state, for example, if there are two threads, T1 and T2 where T1 needs to communicate to the camera and the other thread T2 already using a camera to scan then T1 waits until T2 Thread completes its work, at this state T1 is parked in waiting for the state, and in another scenario, the user called two Threads T2 and T3 with the same functionality and both had same time slice given by Thread Scheduler then both Threads T1, T2 is in a blocked state. When there are multiple threads parked in a Blocked/Waiting state Thread Scheduler clears Queue by rejecting unwanted Threads and allocating CPU on a priority basis.
4. Timed Waiting State
Sometimes the longer duration of waiting for threads causes starvation, if we take an example like there are two threads T1, T2 waiting for CPU and T1 is undergoing a Critical Coding operation and if it does not exist the CPU until its operation gets executed then T2 will be exposed to longer waiting with undetermined certainty, In order to avoid this starvation situation, we had Timed Waiting for the state to avoid that kind of scenario as in Timed Waiting, each thread has a time period for which sleep() method is invoked and after the time expires the Threads starts executing its task.
5. Terminated State
A thread will be in Terminated State, due to the below reasons:
- Termination is achieved by a Thread when it finishes its task Normally.
- Sometimes Threads may be terminated due to unusual events like segmentation faults, exceptions…etc. and such kind of Termination can be called Abnormal Termination.
- A terminated Thread means it is dead and no longer available.
What is Main Thread?
As we are familiar, we create Main Method in each and every Java Program, which acts as an entry point for the code to get executed by JVM, Similarly in this Multithreading Concept, Each Program has one Main Thread which was provided by default by JVM, hence whenever a program is being created in java, JVM provides the Main Thread for its Execution.
How to Create Threads using Java Programming Language?
We can create Threads in java using two ways, namely :
1. By Extending Thread Class
We can run Threads in Java by using Thread Class, which provides constructors and methods for creating and performing operations on a Thread, which extends a Thread class that can implement Runnable Interface. We use the following constructors for creating the Thread:
- Thread
- Thread(Runnable r)
- Thread(String name)
- Thread(Runnable r, String name)
Sample code to create Threads by Extending Thread Class: