Runnable и Thread
В Java многопоточность программы организуется с помощью интерфейса Runnable и класса Thread, который наследуется от Runnable. Первый способ более гибкий, второй – проще.
Та часть кода, которая должна выполняться в отдельном потоке, выносится в свой класс, имеющий переопределенный метод run(). Код метода run() выполняется, когда к объекту типа Thread применяется метод start(). Непосредственный вызов run() новый поток не создает.
Рассмотрим пример:
public class ThreadTest { public static void main(String[] args) throws InterruptedException { AnotherRun anotherRun = new AnotherRun(); Thread childTread = new Thread(anotherRun); childTread.start(); for (int i = 0; i < 3; i++) { System.out.println("m" + i); Thread.sleep(1000); } childTread.join(); System.out.println("End"); } } class AnotherRun implements Runnable { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("r" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("Interrupt"); } } } }
Результат выполнения:
m0 r0 m1 r1 m2 r2 r3 r4 End
Здесь обработка исключений необходима из-за статического метода sleep(), который приостанавливает выполнение текущего потока. Данный метод часто используют в дочерних потоках, когда они должны выполнять какое-либо действие постоянно, но не бесперебойно. Например, периодически проверять доступность ресурса.
Метод join() заставляет текущий поток ждать завершения нити, к которой применяется. Только после этого текущий поток может продолжить выполнение своего кода.
В данном случае мы создаем класс-наследник от Runnable. Объект типа Runnable или его производное передается в конструктор объекта типа Thread. После этого поток запускается.
Другой вариант – когда пользовательский класс является наследником Thread:
public class ThreadTest1 { public static void main(String[] args) throws InterruptedException { AnotherTask thread = new AnotherTask(); thread.start(); for (int i = 0; i < 3; i++) { System.out.println("m" + i); Thread.sleep(1000); } thread.join(); System.out.println("End"); } } class AnotherTask extends Thread { @Override public void run(){ for (int i = 0; i < 5; i++) { System.out.println("r" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("Interrupt"); } } } }
Этот вариант не подходит, если класс для организации отдельного потока должен наследоваться от другого класса (не Thread). Поскольку в Java нет множественного наследования классов, приходится использовать наследование от интерфейса Runnable. Также данный подход не дает возможности запускать несколько потоков на основе одного объекта. Так в первом примере мы могли бы передать единственный объект anotherRun в несколько объектов типа Thread.
Напомним, библиотечный класс Thread сам является наследником Runnable.
Если в отдельный поток обособляется небольшая подзадача, можно использовать неименованный класс:
public class ThreadNoName { public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println("hi"); } }); thread.start(); Thread thread1 = new Thread() { @Override public void run() { System.out.println("hello"); } }; thread1.start(); } }
Прерывание потоков
Для прерывания выполнения нити, если это необходимо, используется метод interrupt(), который устанавливает переменную isInterrupt в значение true. К коде пользовательского класса, унаследованного от Runnable/Thread, это переменная должна проверяться. Отсюда следует, что на самом деле в Java нет возможности прервать поток извне, поток может остановиться только сам.
С другой стороны, в метод sleep() уже встроена проверка переменной isInterrupt, поэтому проверку вручную опускают. Если sleep() считывает наличие прерывания, то генерирует исключение.
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class ThreadInterrupt { public static void main(String[] args) throws IOException { Thread thread = new InterruptedClass(); thread.start(); BufferedReader reader = new BufferedReader( new InputStreamReader(System.in)); reader.readLine(); thread.interrupt(); } } class InterruptedClass extends Thread { @Override public void run() { while (true) { System.out.print("a"); try { sleep(1000); } catch (InterruptedException e) { break; } } } }
В примере основной поток ожидает ввод данных, в это время выполняется вторая нить. Но как только вы нажмете Enter, выполнится метод interrupt(). В свою очередь метод sleep() прочитает значение переменной isInterrupt класса Thread и сгенерирует исключение InterruptedException.
Если sleep() не используется, то isInterrupt проверяется вручную методом isInterrupted(). Следующий пример содержит ошибку, приводящую к зацикливанию:
public class ThreadInterrupt2 { static int a = 1; public static void main(String[] args) throws InterruptedException { Thread thread = new WithoutSleep(); thread.start(); Thread.sleep(2000); thread.interrupt(); } } class WithoutSleep extends Thread { @Override public void run() { while (true) { System.out.println("hi"); } } }
Мы могли бы ожидать, что через 2 секунды сработает метод interrupt(), который прервет дочернюю нить. Однако, поскольку в ней не проверяется значение isInterrupt, цикл продолжает работать. Корректный код может выглядеть так:
class WithoutSleep extends Thread { @Override public void run() { while (!this.isInterrupted()) { System.out.println("hi"); } } }
При наследовании от Runnable текущий поток через this получить нельзя. Его получают, вызывая соответствующий метод класса Thread:
class WithoutSleep2 implements Runnable { @Override public void run() { Thread cur = Thread.currentThread(); while (!cur.isInterrupted()) { System.out.println("hi"); } } }