1. Класс Thread: первый поток в Java
В Java каждый поток исполнения представлен объектом класса Thread. Этот класс — как дирижёр в оркестре: именно он управляет запуском, остановкой и жизненным циклом потока.
Чтобы запустить поток, нужно:
- Создать объект класса Thread (или его наследника).
- Вызвать у этого объекта метод start().
Давайте разберёмся на практике.
Пример 1. Наследование от Thread
Самый прямолинейный способ создать поток — унаследовать свой класс от Thread и переопределить его метод run(). Всё, что вы напишете внутри run(), выполнится в отдельном потоке.
// Простой поток через наследование
public class HelloThread extends Thread {
@Override
public void run() {
System.out.println("Привет из потока! Меня зовут: " + getName());
}
}
public class Main {
public static void main(String[] args) {
HelloThread thread = new HelloThread(); // создаём объект-поток
thread.start(); // запускаем поток
System.out.println("Главный поток завершает работу.");
}
}
Что происходит на самом деле?
Когда вы вызываете thread.start(), Java создаёт новый поток и уже внутри него запускает метод run(). При этом основной поток (тот самый, который начал с main) не ждёт и продолжает свою работу параллельно.
И главное предупреждение: не путайте start() с прямым вызовом run(). Если написать thread.run(), никакого нового потока не будет — это просто обычный метод, который выполнится в том же самом потоке, где вы его вызвали. Настоящее многопоточие начинается только с start().
Что такое getName()?
Метод getName() возвращает имя потока. По умолчанию Java даёт потокам имена вида "Thread-0", "Thread-1" и т.д. Это удобно для отладки.
2. Интерфейс Runnable: лучший способ для большинства случаев
Java — язык, который любит гибкость. Класс Thread уже наследует от Object, а в Java нет множественного наследования классов. Если вы унаследуете свой класс от Thread, то не сможете унаследовать его ещё от чего-то (например, если у вас есть свой класс Car, и вы захотите сделать его потоком — придётся выбирать).
Решение: выносить логику потока в отдельный объект, реализующий интерфейс Runnable. Это интерфейс с одним методом run(). Затем вы создаёте объект класса Thread, передаёте ему свой Runnable — и запускаете поток.
Пример 2. Класс с Runnable
// Класс, реализующий Runnable
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println("Привет из Runnable! Поток: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Runnable runnable = new HelloRunnable();
Thread thread = new Thread(runnable); // передаём Runnable в Thread
thread.start();
System.out.println("Главный поток завершает работу.");
}
}
Здесь мы создаём класс HelloRunnable, который реализует интерфейс Runnable. В методе main создаётся объект этого класса, и он передаётся в конструктор Thread. После этого поток запускается с помощью вызова start().
Отдельно стоит упомянуть метод Thread.currentThread(). Это статический метод, который позволяет получить объект текущего потока и тем самым понять, где именно выполняется наш код.
Почему это круче?
- Ваш класс может наследоваться от любого другого класса (а не только от Thread).
- Можно переиспользовать один и тот же Runnable для запуска в нескольких потоках.
- Код становится более гибким и чистым.
3. Синтаксис: анонимные классы и лямбда-выражения
Java не стоит на месте! После Java 8 мы можем писать ещё короче и удобнее.
Пример 3. Анонимный класс
Иногда не хочется создавать отдельный файл для класса, который нужен один раз. Можно объявить его прямо в месте использования — это называется анонимный класс.
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Анонимный Runnable! Поток: " + Thread.currentThread().getName());
}
});
thread.start();
System.out.println("Главный поток завершает работу.");
}
}
Пример 4. Лямбда-выражение (Java 8+)
Интерфейс Runnable — функциональный (всего один абстрактный метод). Поэтому можно использовать лямбда-выражение:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Лямбда-поток! Поток: " + Thread.currentThread().getName());
});
thread.start();
System.out.println("Главный поток завершает работу.");
}
}
Кратко:
Runnable реализуется «на месте», а лямбда-выражение предоставляет реализацию метода run().
4. Запуск нескольких потоков
Запускать можно сколько угодно потоков (ну, почти — всё зависит от ресурсов компьютера).
Пример 5. Запускаем несколько потоков
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
int threadNumber = i; // обязательно делать копию переменной для лямбды!
Thread thread = new Thread(() -> {
System.out.println("Поток #" + threadNumber + ": привет!");
});
thread.start();
}
System.out.println("Главный поток завершает работу.");
}
}
Вывод может быть разным!
Потоки работают параллельно — кто первым выведет своё сообщение, зависит от ОС и планировщика потоков.
5. Жизненный цикл потока
Понимание того, как живёт поток, важно для отладки и правильного использования потоков.
Основные состояния потока
- NEW — поток создан, но ещё не запущен (new Thread(...)).
- RUNNABLE — поток запущен (start()), может выполняться.
- TERMINATED — поток завершил выполнение (метод run() закончился).
Что происходит при вызове start() и run()
- start() — создаёт новый поток и вызывает его метод run() в этом новом потоке.
- run() — обычный метод; если вызвать напрямую, он выполнится в текущем потоке (не создаст новый поток!).
Пример:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("run() из потока: " + Thread.currentThread().getName()));
thread.run(); // ВЫЗЫВАЕТСЯ В ГЛАВНОМ ПОТОКЕ!
thread.start(); // ВЫЗЫВАЕТСЯ В ОТДЕЛЬНОМ ПОТОКЕ!
}
}
6. Практика: делаем простое многопоточное приложение
Давайте продолжим развивать наше учебное приложение — допустим, мы пишем простую систему логирования, где каждый поток пишет своё сообщение.
public class LoggerTask implements Runnable {
private final String message;
public LoggerTask(String message) {
this.message = message;
}
@Override
public void run() {
System.out.println("[" + Thread.currentThread().getName() + "] Лог: " + message);
}
}
public class Main {
public static void main(String[] args) {
Thread logger1 = new Thread(new LoggerTask("Запуск системы"));
Thread logger2 = new Thread(new LoggerTask("Загрузка данных"));
Thread logger3 = new Thread(new LoggerTask("Обработка запроса"));
logger1.start();
logger2.start();
logger3.start();
System.out.println("Главный поток завершает работу.");
}
}
Что происходит?
Три потока параллельно пишут свои сообщения в консоль. Порядок сообщений не гарантируется — это и есть суть многопоточности.
7. Полезные нюансы
Таблица: сравнение способов создания потоков
| Способ | Гибкость | Переиспользование | Рекомендуется? |
|---|---|---|---|
| Наследование от Thread | Низкая | Нет | Только для простых/учебных примеров |
| Реализация Runnable | Высокая | Да | Да |
| Анонимный класс | Средняя | Нет | Для одноразовых задач |
| Лямбда-выражение | Высокая | Нет | Да (Java 8+) |
Схема: как работает запуск потока
+----------------------+
| main (главный поток)|
+----------------------+
|
v
+----------------------+
| Thread thread = ... |
+----------------------+
|
v
+----------------------+
| thread.start() | ← Поток "оживает"
+----------------------+
|
v
+----------------------+
| run() в новом потоке|
+----------------------+
|
v
+----------------------+
| Поток завершён |
+----------------------+
8. Типичные ошибки при запуске потоков
Ошибка №1: Вызов run() вместо start().
Очень частая ошибка новичков — вызвать у объекта потока метод run() напрямую. В этом случае никакого нового потока не создаётся, метод просто выполняется в текущем (главном) потоке. Правильно: всегда использовать start() для запуска потока.
Ошибка №2: Повторный запуск потока.
Нельзя вызвать start() у одного и того же объекта Thread более одного раза — это вызовет IllegalThreadStateException. Если вам нужно снова запустить задачу, создайте новый объект Thread.
Ошибка №3: Изменение общих переменных без синхронизации.
Если несколько потоков обращаются к одним и тем же переменным, результат может быть неожиданным (race condition). Про синхронизацию поговорим позже, но пока — будьте осторожны.
Ошибка №4: Не учли завершение потоков.
Если важно дождаться завершения потока, используйте метод join(). Иначе основной поток может завершиться раньше, чем дочерние.
Ошибка №5: Путают имя потока и имя класса.
Метод getName() возвращает имя потока, а не имя класса. Для отладки всегда полезно явно задавать имена потокам через конструктор или setName().
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ