JavaRush /Java блог /Random UA /Thread'ом Java не зіпсуєш: Частина I - потоки
Viacheslav
3 рівень

Thread'ом Java не зіпсуєш: Частина I - потоки

Стаття з групи Random UA

Вступ

Багатопотоковість в Java була закладена з перших днів. Тому давайте коротко ознайомимося з тим, про що це багатопоточність. Thread'ом Java не зіпсуєш: Частина I - потоки - 1Візьмемо за точку відліку офіційний урок від 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.javajava HelloWorldApp Roger Thread'ом Java не зіпсуєш: Частина I - потоки - 2
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 процесі, використовуємо програму, яка входить до постачання 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 в 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
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ