JavaRush /Blog Java /Random-VI /Thread'ом Java не испортишь: Часть I — потоки
Viacheslav
Mức độ

Thread'ом Java не испортишь: Часть I — потоки

Xuất bản trong nhóm

Вступление

Многопоточность в Java была заложена с самых первых дней. Поэтому давайте кратко ознакомимся с тем, про что это — многопоточность. Thread'ом Java не испортишь: Часть I — потоки - 1Возьмём за точку отсчёта официальный урок от Oracle: "Lesson: The "Hello World!" Application". Код нашего Hello World applications немного изменим на следующий:

class HelloWorldApp {
    public static void main(String[] args) {
        System.out.println("Hello, " + args[0]);
    }
}
args — это массив входных параметров, передаваемых при запуске программы. Сохраним данный code в файл с именем, которое совпадает с именем класса и с расширением .java. Скомпorруем при помощи утorты javac: javac HelloWorldApp.java После этого вызовем наш code с Howим-нибудь параметром, например, Roger: java HelloWorldApp Roger Thread'ом Java не испортишь: Часть I — потоки - 2У нашего codeа сейчас есть серьёзный изъян. Если не передать ниHowой аргумент (т.е. выполнить просто java HelloWorldApp), мы получим ошибку:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
        at HelloWorldApp.main(HelloWorldApp.java:3)
Возникло исключение (т.е. ошибка) в thread (в потоке) с именем main. Получается, в Java есть Howие-то потоки? Отсюда начинается наш путь.

Java и потоки

Whatбы разобраться, что такое поток, надо понять, How происходит запуск Java applications. Давайте изменим наш code следующим образом:

class HelloWorldApp {
    public static void main(String[] args) {
		while (true) { 
			//Do nothing
		}
	}
}
Теперь давайте скомпorруем это снова при помощи javac. Далее для удобства запустим наш Java code в отдельном окне. В Windows это можно сделать так: start java HelloWorldApp. Теперь при помощи утorты jps посмотрим, Howую информацию нам сообщит Java: Thread'ом Java не испортишь: Часть I — потоки - 3Первое число — это PID or Process ID, идентификатор процесса. What такое процесс?

Процесс — это совокупность codeа и данных, разделяющих общее виртуальное addressное пространство.
При помощи процессов выполнение разных программ изолировано друг от друга: каждое приложение использует свою область памяти, не мешая другим программам. Более подробно советую ознакомиться в статье: "https://habr.com/post/164487/". Процесс не может существовать без потоков, поэтому если существует процесс, в нём существует хотя бы один поток. Как же это происходит в Java? Когда мы запускаем Java программу, ее выполнение начинается с метода main. Мы How бы входим в программу, поэтому этот особый метод main называется точкой входа, or "entry point". Метод main всегда должен быть public static void, чтобы виртуальная машина Java (JVM) смогла начать выполнение нашей программы. Подробнее см. "Why is the Java main method static?". Получается, что java launcher (java.exe or javaw.exe) — это простое приложение (simple C application): оно загружает различные DLL, которые на самом деле являются JVM. Java launcher выполняет определённый набор Java Native Interface (JNI) вызовов. JNI — это механизм, соединяющий мир виртуальной машины Java и мир C++. Получается, что launcher — это не JVM, а её загрузчик. Он знает, Howие правильные команды нужно выполнить, чтобы запустилась JVM. Знает, How организовать всё необходимое окружение при помощи JNI вызовов. В эту организацию окружения входит и создание главного потока, который обычно называется main. Whatбы нагляднее рассмотреть, Howие живут потоки в java процессе, используем программу jvisualvm, которая входит в поставку JDK. Зная pid процесса, мы можем открыть данные сразу по нему: jvisualvm --openpid айдипроцесса Thread'ом Java не испортишь: Часть I — потоки - 4Интересно, что каждый поток имеет свою обособленную область в памяти, выделенной для процесса. Эту структуру памяти называют стеком. Стек состоит из фрэймов. Фрэйм — это точка вызова метода, execution point. Также фрэйм может быть представлен How 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 типа потоков: демоны и не демоны. Демон-потоки — это фоновые потоки (служебные), выполняющие Howую-то работу в фоне. Такой интересный термин — это отсылка к "демону Максвелла", о чём подробнее можно прочитать в википедии в статье про "демонов". Как сказано в documentации, JVM продолжает выполнение программы (процесса), до тех пор, пока:
  • Не вызван метод Runtime.exit
  • Все НЕ демон-потоки завершor свою работу (How без ошибок, так и с выбрасыванием исключений)
Отсюда и важная деталь: демон-потоки могут быть завершены на любой выполняемой команде. Поэтому целостность данных в них не гарантируется. Поэтому, демон потоки подходят для Howих-то служебных задач. Например, в Java есть поток, который отвечает за обработку методов finalize or потоки, относящиеся к сборщику мусора (Garbage Collector, GC). Каждый поток входит в Howую-то группу (ThreadGroup). А группы могут входит друг в друга, образовывая некоторую иерархию or структуру.

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("An error occurred: " + e.getMessage());
		}
	});
    System.out.println(2/0);
}
Деление на ноль вызовет ошибку, которая будет перехвачена обработчиком. Если обработчик не указывать самому, отработает реализация обработчика по умолчанию, которая будет в StdError выводить стэк ошибки. Подробнее можно прочитать в обзоре http://pro-java.ru/java-dlya-opytnyx/obrabotchik-neperexvachennyx-isklyuchenij-java/". Кроме того, у потока есть приоритет. Подробнее про приоритеты можно прочитать в статье "Java Thread Priority in Multithreading".

Creation потока

Как и сказано в documentации, у нас 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 напрямую, ниHowой новый поток не будет запущен. Именно метод 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. Это позволяет писать code задач для потоков ещё красивее:

public static void main(String []args){
	Runnable task = () -> { 
		System.out.println("Hello, World!");
	};
	Thread thread = new Thread(task);
	thread.start();
}

Total

Итак, надеюсь, из сего повестования понятно, что такое поток, How они существуют и Howие базовые операции с ними можно выполнять. В следующей части стоит разобраться, How потоки взаимодействуют друг с другом и Howой у них vital цикл. #Viacheslav
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION