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");
        }
    }
}

Создано