Вступ
Багатопоточність у Java була закладена з перших днів. Тому давайте коротко ознайомимось із тим, про що це — багатопоточність.
Код нашого Hello World додатку трохи змінимо на наступний:
class HelloWorldApp {
public static void main(String[] args) {
System.out.println("Hello, " + args[0]);
}
}
args — це масив вхідних параметрів, переданих під час запуску програми. Збережемо цей код у файл з іменем, яке збігається з іменем класу і з розширенням
.java. Скомпілюємо за допомогою утиліти
javac:
javac HelloWorldApp.java
Після цього викличемо наш код із якимось параметром, наприклад, Roger:
java HelloWorldApp Roger
![Thread'ом Java не зіпсуєш: Частина I — потоки - 2]()
У нашого коду зараз є серйозний недолік. Якщо не передати жодного аргументу (тобто виконати просто java HelloWorldApp), ми отримаємо помилку:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at HelloWorldApp.main(HelloWorldApp.java:3)
Виникла виключна ситуація (тобто помилка) у thread (потоці) з ім’ям
main.
Виходить, у Java є якісь потоки? Звідси починається наш шлях.
Java і потоки
Щоб зрозуміти, що таке потік, треба зрозуміти, як відбувається запуск Java додатку.
Давайте змінимо наш код таким чином:
class HelloWorldApp {
public static void main(String[] args) {
while (true) {
//Do nothing
}
}
}
Тепер давайте скомпілюємо це знову за допомогою javac.
Далі для зручності запустимо наш Java код у окремому вікні. У Windows це можна зробити так:
start java HelloWorldApp.
Тепер за допомогою утиліти
jps подивимося, яку інформацію нам повідомить Java:
![Thread'ом Java не зіпсуєш: Частина I — потоки - 3]()
Перше число — це 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 айдіпроцесу
![Thread'ом Java не зіпсуєш: Частина I — потоки - 4]()
Цікаво, що кожен потік має свою окрему область у пам’яті, виділеній для процесу. Цю структуру пам’яті називають стеком.
Стек складається з фреймів. Фрейм — це точка виклику методу, execution point. Також фрейм може бути представлений як StackTraceElement (див. Java API для
StackTraceElement).
Детальніше про пам’ять, виділену кожному потоку, можна прочитати
тут.
Якщо подивитись на
Java API і пошукати там слово Thread, ми побачимо, що є клас
java.lang.Thread.
Саме цей клас представляє у Java потік, і з ним нам і доведеться працювати.
![Thread'ом Java не зіпсуєш: Частина I — потоки - 5]()
java.lang.Thread
Потік у Java представлений у вигляді екземпляра класу
java.lang.Thread. Варто одразу розуміти, що екземпляри класу Thread у Java самі по собі не є потоками. Це лише свого роду API для низькорівневих потоків, якими керує JVM та операційна система. Коли за допомогою java launcher'а ми запускаємо JVM, вона створює головний потік з ім’ям
main і ще кілька службових потоків.
Як сказано в JavaDoc класу Thread:
When a Java Virtual Machine starts up, there is usually a single non-daemon thread
Існує 2 типи потоків: демони і не демони. Демон-потоки — це фонові потоки (службові), які виконують якусь роботу у фоні. Такий цікавий термін — це відсилання до "демона Максвелла", про що докладніше можна прочитати у вікіпедії в статті про "
демонів".
Як сказано в документації, JVM продовжує виконання програми (процесу) доти, доки:
- Не викликано метод Runtime.exit
- Усі НЕ демон-потоки завершили свою роботу (як без помилок, так і з викиданням виключень)
Отже і важлива деталь: демон-потоки можуть бути завершені на будь-якій виконуваній команді. Тому цілісність даних у них не гарантується. Тому, демон-потоки підходять для якихось службових задач. Наприклад, у Java є потік, який відповідає за обробку методів finalize або потоки, що належать до збирача сміття (Garbage Collector, GC).
Кожен потік входить в якусь групу (
ThreadGroup).
А групи можуть входити одна в одну, утворюючи певну ієрархію чи структуру.
public static void main(String []args){
Thread currentThread = Thread.currentThread();
ThreadGroup threadGroup = currentThread.getThreadGroup();
System.out.println("Thread: " + currentThread.getName());
System.out.println("Thread Group: " + threadGroup.getName());
System.out.println("Parent Group: " + threadGroup.getParent().getName());
}
Групи дозволяють впорядкувати управління потоками та вести їх облік.
Крім груп, у потоків є свій обробник виключень. Подивимося на приклад:
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, який ми перевизначаємо:
public void run() {
if (target != null) {
target.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();
}
Підсумок
Отже, сподіваюся, з цього опису зрозуміло, що таке потоки, як вони існують і які базові операції з ними можна виконувати.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ