В работающей Java-программе весь код выполняется в потоках, а внутри потока все происходит последовательно, одна инструкция за другой. Когда Java (или, скорее, JVM ) запускается, она создает один поток для main
метода, который будет выполняться. Оттуда могут быть созданы новые потоки для выполнения кода параллельно основному. Самый простой способ сделать это — использовать класс Thread
.
Эта статья не требует никаких знаний о многопоточном программировании, но вы должны быть знакомы с основными понятиями Java, такими как классы и интерфейсы .
Основы темы
Java предлагает класс Thread
который можно использовать для запуска новых потоков, ожидания их завершения или взаимодействия с ними более сложными способами, выходящими за рамки этой статьи.
Создание тем
Чтобы создать поток, вам нужно определить блок кода, реализовав интерфейс Runnable
, в котором работает только один абстрактный метод. Экземпляр этой реализации затем может быть передан в конструктор Thread
.
Давайте начнем с примера, который печатает три сообщения и ждет полсекунды после каждой печати.
class PrintingRunnable implements Runnable { private final int id; public PrintingRunnable(int id) { this.id = id; } @Override // This function will be executed in parallel public void run() { try { // Print a message five times for (int i = 0; i < 3; i++) { System.out.println("Message " + i + " from Thread " + id); // Wait for half a second (500ms) Thread.sleep(500); } } catch (InterruptedException ex) { System.out.println("Thread was interrupted"); } } }
InterruptedException
вызывается Thread.sleep()
. Работа с ним может быть деликатной, но я не буду вдаваться в подробности — вы можете прочитать об этом в этой статье .
Мы можем использовать реализацию Runnable
для создания экземпляра Thread
.
Thread thread = new Thread(new PrintingRunnable(1));
Запуск темы
С потоком в руке, пришло время запустить его:
System.out.println("main() started"); Thread thread = new Thread(new PrintingRunnable(1)); thread.start(); System.out.println("main() finished");
Если вы запустите этот код, вы должны увидеть вывод, похожий на этот:
main() started Message 0 from Thread 1 main() finished Message 1 from Thread 1 Message 2 from Thread 1
Обратите внимание, что сообщения от main
метода и потока, который мы начали, чередуются. Это потому, что они работают параллельно, а их выполнение чередуется непредсказуемо. На самом деле, скорее всего, вы увидите немного разные результаты при каждом запуске программы. В то же время, инструкции внутри одного потока всегда выполняются в ожидаемом порядке, как вы можете видеть по возрастающим номерам сообщений.
Если вы до сих пор видели только последовательный код Java, вы можете быть удивлены тем, что программа продолжала работать после завершения main
метода. Фактически, поток, который выполняет метод main
не обрабатывается каким-либо особым образом, и JVM завершит выполнение программы, как только все потоки будут завершены.
Мы также можем запустить несколько потоков одним и тем же подходом:
System.out.println("main() started"); for (int i = 0; i < 3; i++) { Thread thread = new Thread(new PrintingRunnable(i)); thread.start(); } System.out.println("main() finished");
В этом случае все три потока будут выводить сообщения параллельно:
main() started Message 0 from Thread 0 main() finished Message 0 from Thread 1 Message 0 from Thread 2 Message 1 from Thread 0 Message 1 from Thread 2 Message 1 from Thread 1 Message 2 from Thread 1 Message 2 from Thread 0 Message 2 from Thread 2
Отделочные нити
Итак, когда заканчивается поток? Это происходит в одном из двух случаев:
- все инструкции в
Runnable
выполняются - необработанное исключение выдается из метода
run
Пока что мы столкнулись только с первым случаем. Чтобы увидеть, как работает второй вариант, я реализовал Runnable
который печатает сообщение и выдает исключение:
class FailingRunnable implements Runnable { @Override public void run() { System.out.println("Thread started"); // The compiler can detect when some code cannot be reached // so this "if" statement is used to trick the compiler // into letting me write a println after throwing if (true) { throw new RuntimeException("Stopping the thread"); } System.out.println("This won't be printed"); } }
Теперь давайте запустим этот код в отдельном потоке:
Thread thread = new Thread(new FailingRunnable()); thread.start(); System.out.println("main() finished");
Это вывод:
Thread started Exception in thread "Thread-0" java.lang.RuntimeException: Stopping the thread at com.example.FailingRunnable.run(App.java:58) at java.lang.Thread.run(Thread.java:745) main() finished
Как вы можете видеть, последний оператор println
не был выполнен, потому что JVM остановила выполнение потока, как только было сгенерировано исключение.
Вы можете быть удивлены тем, что main
функция продолжила выполнение, несмотря на выданное исключение. Это связано с тем, что разные потоки не зависят друг от друга, и если один из них выходит из строя, другие продолжат работу, как будто ничего не произошло.
В ожидании Темы
Одна общая задача, которую нам нужно выполнить с потоком, — это дождаться его завершения. В Java это довольно просто. Все, что нам нужно сделать, это вызвать метод join
для экземпляра потока:
System.out.println("main() started"); Thread thread = new Thread(new PrintingRunnable(1)); thread.start(); System.out.println("main() is waiting"); thread.join(); System.out.println("main() finished");
В этом случае вызывающий поток будет заблокирован, пока целевой поток не будет завершен. Когда выполняется последняя инструкция в целевом потоке, вызывающий поток возобновляется:
main() started main() is waiting Message 0 from Thread 1 Message 1 from Thread 1 Message 2 from Thread 1 main() finished
Обратите внимание, что «main () Закончено» печатается после всех сообщений из PrintingThread
. Используя метод join
таким образом, мы можем гарантировать, что некоторые операции выполняются строго после всех инструкций в определенном потоке. Если мы вызываем join
в потоке, который уже завершил, вызов немедленно возвращается, и вызывающий поток не приостанавливается. Это облегчает ожидание нескольких потоков, просто зацикливая их коллекцию и вызывая join
для каждого.
Поскольку join
делает вызывающий поток приостановленным, оно также генерирует InterruptedException
.
Выводы
В этом посте. Вы узнали, как создавать независимые потоки инструкций в Java. Чтобы создать поток, вам нужно реализовать интерфейс Runnable
и использовать экземпляр для создания объекта Thread
. Потоки могут быть запущены при start
и его выполнение заканчивается, когда у него заканчиваются инструкции для выполнения или когда он генерирует необработанное исключение. Чтобы дождаться завершения выполнения потока, мы можем использовать метод join
.
Обычно потоки взаимодействуют друг с другом, что может вызвать некоторые неожиданные проблемы, которые не возникают при написании однопоточного кода. Изучите такие темы, как условия гонки , синхронизация , блокировки и одновременные коллекции, чтобы узнать больше об этом.
В настоящее время Thread
считается инструментом низкого уровня в многопоточном программировании. Вместо явного создания потоков вы можете использовать пулы потоков, которые ограничивают количество потоков в вашем приложении, и исполнителей, которые позволяют выполнять асинхронные задачи.