1. Что такое потоки в Java
2. Потоки и многозадачность
3. Создание и управление потоками
4. Жизненный цикл потока
5. Синхронизация потоков
6. Преимущества и недостатки многопоточности
Иногда нужно настроить работу программы таким образом, чтобы она выполняла несколько операций одновременно, ведь это значительно повышает производительность кода. Такого поведения можно добиться, применяя многопоточное программирование в Java.
В этой статье расскажем, что такое потоки в Джава и как их создавать, рассмотрим их жизненный цикл, синхронизацию и, конечно, раскроем преимущества и недостатки многопоточности (multithreading).
Что такое потоки в Java
Поток (ветвь исполнения) в Java — это отдельная последовательность выполнения команд в коде. Каждый из них работает независимо и параллельно с другими. Простой пример: у Маши есть два дела — сделать маме чай и почистить зубы. Она может делать это последовательно, но потратит много времени. Поэтому, пока кипит чайник, она почистит зубы — и распределит время эффективно. Потоки Java работают подобным образом, только могут выполнять куда больше команд одновременно. Современное программирование трудно представить без них.
Потоки и многозадачность
Многозадачность — это способность программ выполнять несколько операций одновременно. Благодаря ей приложения могут выполнять фоновые операции, реагировать на действия пользователей без задержек. В Джава многозадачность реализуется путем создания ветвей исполнения.
Создание и управление потоками
В Джава есть несколько способов создания многопоточного кода, но в этой статье мы рассмотрим два основных: использование class «Thread» и интерфейса «Runnable».
Class «Thread»
Суть способа: необходимо создать класс, который наследуется от Thread. Затем нужно переопределить run(): вставить туда код, который должен быть выполнен в данной ветви исполнения. Далее — объявить класс Main и определить его метод main().
Пример:
// Объявляем класс и переопределяем run()
class NewThread extends Thread {
public void run() {
System.out.println("Привет, мир!");
}
}
// Объявляем класс Main, определяем main(). Cоздаем экземпляр NewThread и вызываем start()
public class Main {
public static void main(String[] args) {
NewThread thread = new NewThread();
thread.start();
}
}
Интерфейс «Runnable»
Суть: необходимо создать класс, реализующий интерфейс «Runnable», реализовать в нем run(). run() должен содержать в себе код, который будет исполняться вместе с основной ветвью программы. Затем нужно объявить класс Main и main().
Пример:
// Объявляем класс NewRunnable и реализуем run()
class NewRunnable implements Runnable {
public void run() {
System.out.println("Привет, мир!");
}
}
// Объявляем класс Main, определяем main(), создаем экземпляр NewRunnable, вызываем start()
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new NewRunnable());
thread.start();
}
}
Заметно, что способы похожи между собой, но все же между ними есть различия, которые необходимо учитывать при программировании. В данной статье мы не будем заострять на этом внимание, лишь скажем, что интерфейс «Runnable» часто предпочтительнее.
Жизненный цикл потока
Для более полного погружения в тему стоит рассмотреть жизненный цикл потока, а точнее — состояния внутри него.
- New — создан, не запущен: для запуска он ждет команду start().
- Runnable — запущен / готов к исполнению / ждет выделения ресурсов (CPU time).
- Blocked — заблокирован, ожидает освобождения ресурса, необходимого для продолжения выполнения операции.
- Waiting и Timed_waiting — ожидание определенного сигнала для продолжения работы. Waiting значит ожидание без временных ограничений, Timed_waiting — с ними.
- Terminated — конечное состояние, выполнение операций завершено.
Синхронизация потоков
Поговорим и о таком важном аспекте, как синхронизация. Она необходима для предупреждения конфликтов при единовременном обращении к общим ресурсам. Под ресурсами мы в том числе понимаем данные, файлы, сетевые и системные ресурсы. Конфликты приводят к несогласованности данных, недетерминированному поведению и мертвым блокировкам.
Рассмотрим самый популярный в современном программировании механизм управления синхронизацией в Java — ключевое слово synchronized. Объявление сущности синхронной делает возможным использовать ее в данный момент только одной ветвью.
Пример объявления метода синхронным (пример бессмысленный, нужен для демонстрации синтаксиса):
class Counter {
private int count = 0;
// Увеличиваем значение счетчика на 1
public synchronized int increment() {
count += 1;
}
// Получаем текущее значение count
public synchronized int getCount() {
return count;
}
Преимущества и недостатки многопоточности
Подведем небольшой итог данной статьи и рассмотрим плюсы и минусы мультипоточности, или multithreading, в Джава.
Преимущества | Недостатки |
Повышение производительности — главный плюс многопоточности, ведь быстрое выполнение сложных операций — залог качественной программы. | Трудности в отладке кода. Создание потоков — сложный процесс, их взаимодействие — еще сложнее, его трудно отследить, поэтому ошибки в таком коде искать проблематично. |
Читаемый и структурированный код. Эти свойства кода значимы для командной разработки, также структурированный код проще поддерживать и расширять. | Важность синхронизации. Программисту необходимо корректно организовать синхронизацию во избежание серьезных дефектов программы. |
Мультипоточность — это правильное распределение таких ресурсов, как память, CPU, входные и выходные устройства и других. | Более сложная архитектура приложения по сравнению с однопоточными приложениями, а значит, большие затраты. |
Улучшение пользовательского опыта. Интерфейс программы остается доступным даже при выполнении сложных фоновых задач. | Программист должен понимать, когда стоит применять многопоточность, а когда нет. Некорректное применение только снизит производительность программы. |