Thread создание потоков java
Для создания нового потока мы можем создать новый класс, либо наследуя его от класса Thread, либо реализуя в классе интерфейс Runnable .
Наследование от класса Thread
Создадим свой класс на основе Thread:
class JThread extends Thread < JThread(String name)< super(name); >public void run() < System.out.printf("%s started. \n", Thread.currentThread().getName()); try< Thread.sleep(500); >catch(InterruptedException e) < System.out.println("Thread has been interrupted"); >System.out.printf("%s fiished. \n", Thread.currentThread().getName()); > > public class Program < public static void main(String[] args) < System.out.println("Main thread started. "); new JThread("JThread").start(); System.out.println("Main thread finished. "); >>
Класс потока называется JThread. Предполагается, что в конструктор класса передается имя потока, которое затем передается в конструктор базового класса. В конструктор своего класса потока мы можем передать различные данные, но главное, чтобы в нем вызывался конструктор базового класса Thread, в который передается имя потока.
И также в JThread переопределяется метод run() , код которого собственно и будет представлять весь тот код, который выполняется в потоке.
В методе main для запуска потока JThread у него вызывается метод start() , после чего начинается выполнение того кода, который определен в методе run:
Main thread started. Main thread finished. JThread started. JThread finished.
Здесь в методе main в конструктор JThread передается произвольное название потока, и затем вызывается метод start() . По сути этот метод как раз и вызывает переопределенный метод run() класса JThread.
Обратите внимание, что главный поток завершает работу раньше, чем порожденный им дочерний поток JThread.
Аналогично созданию одного потока мы можем запускать сразу несколько потоков:
public static void main(String[] args)
Main thread started. Main thread finished. JThread 2 started. JThread 5 started. JThread 4 started. JThread 1 started. JThread 3 started. JThread 1 finished. JThread 2 finished. JThread 5 finished. JThread 4 finished. JThread 3 finished.
Ожидание завершения потока
При запуске потоков в примерах выше Main thread завершался до дочернего потока. Как правило, более распространенной ситуацией является случай, когда Main thread завершается самым последним. Для этого надо применить метод join() . В этом случае текущий поток будет ожидать завершения потока, для которого вызван метод join:
public static void main(String[] args) < System.out.println("Main thread started. "); JThread t= new JThread("JThread "); t.start(); try< t.join(); >catch(InterruptedException e) < System.out.printf("%s has been interrupted", t.getName()); >System.out.println("Main thread finished. "); >
Метод join() заставляет вызвавший поток (в данном случае Main thread) ожидать завершения вызываемого потока, для которого и применяется метод join (в данном случае JThread).
Main thread started. JThread started. JThread finished. Main thread finished.
Если в программе используется несколько дочерних потоков, и надо, чтобы Main thread завершался после дочерних, то для каждого дочернего потока надо вызвать метод join.
Реализация интерфейса Runnable
Другой способ определения потока представляет реализация интерфейса Runnable . Этот интерфейс имеет один метод run :
В методе run() собственно определяется весь тот код, который выполняется при запуске потока.
После определения объекта Runnable он передается в один из конструкторов класса Thread:
Thread(Runnable runnable, String threadName)
Для реализации интерфейса определим следующий класс MyThread:
class MyThread implements Runnable < public void run()< System.out.printf("%s started. \n", Thread.currentThread().getName()); try< Thread.sleep(500); >catch(InterruptedException e) < System.out.println("Thread has been interrupted"); >System.out.printf("%s finished. \n", Thread.currentThread().getName()); > > public class Program < public static void main(String[] args) < System.out.println("Main thread started. "); Thread myThread = new Thread(new MyThread(),"MyThread"); myThread.start(); System.out.println("Main thread finished. "); >>
Реализация интерфейса Runnable во многом аналогична переопределению класса Thread. Также в методе run определяется простейший код, который усыпляет поток на 500 миллисекунд.
В методе main вызывается конструктор Thread, в который передается объект MyThread. И чтобы запустить поток, вызывается метод start() . В итоге консоль выведет что-то наподобие следующего:
Main thread started. Main thread finished. MyThread started. MyThread finished.
Поскольку Runnable фактически представляет функциональный интерфейс, который определяет один метод, то объект этого интерфейса мы можем представить в виде лямбда-выражения:
public class Program < public static void main(String[] args) < System.out.println("Main thread started. "); Runnable r = ()-> < System.out.printf("%s started. \n", Thread.currentThread().getName()); try< Thread.sleep(500); >catch(InterruptedException e) < System.out.println("Thread has been interrupted"); >System.out.printf("%s finished. \n", Thread.currentThread().getName()); >; Thread myThread = new Thread(r,"MyThread"); myThread.start(); System.out.println("Main thread finished. "); > >
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