Вступ
Багатопотоковість в Java була закладена з перших днів. Тому давайте коротко ознайомимося з тим, про що це багатопоточність.
Візьмемо за точку відліку офіційний урок від Oracle: "
Lesson: The "Hello World!" Application ". Код нашого Hello World програми трохи змінимо на наступний:
class HelloWorldApp {
public static void main(String[] args) {
System.out.println("Hello, " + args[0]);
}
}
args
- Це масив вхідних параметрів, що передаються при запуску програми. Збережемо цей код у файл з ім'ям, яке збігається з ім'ям класу та з розширенням
.java
. Після цього викличемо наш код з яким-небудь параметром, наприклад, Roger: У нашого коду
зараз є серйозна вада. Якщо не передати жодного аргументу (тобто виконати просто java HelloWorldApp), ми отримаємо помилку:
javac HelloWorldApp.java
java HelloWorldApp Roger
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) {
}
}
}
Тепер давайте скомпілюємо це знову за допомогою 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 процесі, використовуємо
програму, яка входить до постачання JDK. Знаючи pid процесу, ми можемо відкрити дані відразу по ньому:
jvisualvm --openpid айдипроцесса
Цікаво, що кожен потік має свою відокремлену область пам'яті, виділеної для процесу. Цю структуру пам'яті називають стеком. Стек складається з фреймів. Фрейм це точка виклику методу, execution point. Також кадр може бути представлений як StackTraceElement (див. Java API для
StackTraceElement ). Докладніше про пам'ять, що виділяється кожному потоку, можна прочитати
тут . Якщо подивитися на
Java API і пошукати слово Thread, ми побачимо, що є клас
java.lang.Thread . Саме цей клас представляє в Java потік, і з ним нам і доведеться працювати.
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 в Multithreading ".
Створення потоку
Як і сказано в документації, у нас два способи створити потік. Перший – створити свого спадкоємця. Наприклад:
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();
}
Разом
Отже, сподіваюся, з цієї розповіді зрозуміло, що таке потік, як вони існують і які базові операції з ними можна виконувати. У
наступній частині варто розібратися, як потоки взаємодіють один з одним та який у них життєвий цикл. #Viacheslav
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ