Многопоточное программирование на Java
Java обеспечивает встроенную поддержку многопоточного программирования. Поток — это единый последовательный поток управления внутри процесса. Процесс может иметь несколько потоков, выполняющихся одновременно, и каждый поток выполняет разные задачи параллельно.
Многопоточность — это особая форма многозадачности, но многопоточность использует меньшие накладные расходы ресурсов.
Вот ещё один термин, связанный с потоками, — процесс: процесс включает пространство памяти, выделенное операционной системой, и содержит один или несколько потоков. Поток не может существовать независимо; он должен быть частью процесса. Процесс продолжает выполняться, пока все недемонические потоки не завершат выполнение.
Многопоточность позволяет программистам писать высокоэффективные программы для полного использования CPU.
Жизненный цикл потока
Поток — это динамический процесс выполнения, и он также проходит путь от создания до смерти.
Следующая диаграмма показывает полный жизненный цикл потока.
- Новое состояние:
Используя ключевое словоnewиThreadПосле того как класс или его подкласс создает объект потока, объект потока находится в новом состоянии. Он остается в этом состоянии до тех пор, пока программа не вызоветstart()этот поток.
- Состояние готовности:
После того как объект потока вызывает метод start(), поток переходит в состояние готовности. Поток в состоянии готовности находится в очереди готовности и ожидает планирования планировщиком потоков в JVM.
- Состояние выполнения:
Если поток в состоянии готовности получает ресурсы CPU, он может выполнятьrun(), в этот момент поток находится в состоянии выполнения. Поток в состоянии выполнения является наиболее сложным; он может переходить в заблокированное состояние, состояние готовности и мертвое состояние.
- Заблокированное состояние:
Если поток выполняет такие методы, как sleep, suspend и так далее, после потери занятых ресурсов поток переходит из состояния выполнения в заблокированное состояние. Когда время sleep истекает или получены ресурсы устройства, он может снова перейти в состояние готовности. Его можно разделить на три типа:
Ожидающая блокировка: поток в состоянии выполнения выполняет метод wait(), что приводит к переходу потока в состояние ожидающей блокировки.
Синхронная блокировка: поток не может получить блокировку synchronized (потому что блокировка занята другим потоком).
Прочая блокировка: когда запрос I/O выдается путем вызова sleep() или join() потока, поток переходит в заблокированное состояние. Когда время состояния sleep() истекает, join() ожидает завершения потока или истечения времени ожидания, либо когда обработка I/O завершается, поток возвращается в состояние готовности.
- Состояние завершения:
Когда выполняющийся поток завершает свою задачу или возникают другие условия завершения, поток переходит в состояние завершения.
Приоритет потока
Каждый поток Java имеет приоритет, который помогает операционной системе определить порядок планирования потоков.
Приоритет потока Java — это целое число, значение которого находится в диапазоне от 1 (Thread.MIN_PRIORITY) до 10 (Thread.MAX_PRIORITY).
По умолчанию каждому потоку назначается приоритет NORM_PRIORITY (5).
Потоки с более высоким приоритетом более важны для программы, и им должны выделяться ресурсы процессора раньше, чем потокам с более низким приоритетом. Однако приоритет потока не гарантирует порядок выполнения потоков и сильно зависит от платформы.
Создание потока
Java предоставляет три способа создания потоков:
- Путем реализации интерфейса Runnable;
- Путем расширения самого класса Thread;
- Путем создания потоков через Callable и Future.
Создание потока путём реализации интерфейса Runnable
Чтобы создать поток, проще всего создать класс, реализующий интерфейс Runnable.
Чтобы реализовать Runnable, классу достаточно реализовать один метод — run(), объявленный следующим образом:
Вы можете переопределить этот метод. Важно понимать, что run() может вызывать другие методы, использовать другие классы и объявлять переменные, как и главный поток.
После создания класса, реализующего интерфейс Runnable, вы можете создать экземпляр объекта потока внутри этого класса.
Класс Thread определяет несколько конструкторов. Ниже приведён тот, который мы часто используем:
Здесь threadOb — это экземпляр класса, реализующего интерфейс Runnable, а threadName задаёт имя нового потока.
После создания нового потока он начнёт выполняться только при вызове его метода start().
Ниже приведён пример создания потока и его запуска:
Пример
Скомпилируйте вышеуказанную программу. Результат выполнения следующий:
Creating Thread-1 Starting Thread-1 Creating Thread-2 Starting Thread-2 Running Thread-1 Thread: Thread-1, 4 Running Thread-2 Thread: Thread-2, 4 Thread: Thread-1, 3 Thread: Thread-2, 3 Thread: Thread-1, 2 Thread: Thread-2, 2 Thread: Thread-1, 1 Thread: Thread-2, 1 Thread Thread-1 exiting. Thread Thread-2 exiting.
Создание потока путем расширения Thread
Второй способ создания потока — создать новый класс, расширяющий класс Thread, а затем создать экземпляр этого класса.
Расширяющий класс должен переопределить метод run(), который является точкой входа нового потока. Он также должен вызвать метод start() для выполнения.
Хотя этот метод указан как подход к реализации многопоточности, по сути, он представляет собой экземпляр, реализующий интерфейс Runnable.
Пример
Скомпилируйте вышеуказанную программу. Результат выполнения следующий:
Creating Thread-1 Starting Thread-1 Creating Thread-2 Starting Thread-2 Running Thread-1 Thread: Thread-1, 4 Running Thread-2 Thread: Thread-2, 4 Thread: Thread-1, 3 Thread: Thread-2, 3 Thread: Thread-1, 2 Thread: Thread-2, 2 Thread: Thread-1, 1 Thread: Thread-2, 1 Thread Thread-1 exiting. Thread Thread-2 exiting.
Методы Thread
В следующей таблице перечислены некоторые важные методы класса Thread:
| Нет. | Описание метода |
|---|---|
| 1 |
public void start() Заставляет этот поток начать выполнение;JavaВиртуальная машина Java вызывает метод run этого потока. |
| 2 |
public void run() Если этот поток был создан с использованием отдельного объекта run типа Runnable, то вызывается метод run этого объекта Runnable; в противном случае этот метод ничего не делает и возвращает управление. |
| 3 |
public final void setName(String name) Изменяет имя этого потока так, чтобы оно совпадало с именем аргумента. |
| 4 |
public final void setPriority(int priority) Изменяет приоритет этого потока. |
| 5 |
public final void setDaemon(boolean on) Помечает этот поток как поток-демон или пользовательский поток. |
| 6 |
public final void join(long millisec) Ожидает завершения этого потока не более millis миллисекунд. |
| 7 |
public void interrupt() Прерывает этот поток. |
| 8 |
public final boolean isAlive() Проверяет, жив ли этот поток. |
Приведённые выше методы вызываются объектами Thread. Методы в следующей таблице являются статическими методами класса Thread.
| Нет. | Описание метода |
|---|---|
| 1 |
public static void yield() Приостанавливает текущий выполняющийся объект потока и позволяет выполняться другим потокам. |
| 2 |
public static void sleep(long millisec) Заставляет текущий выполняющийся поток перейти в спящий режим (приостановить выполнение) на указанное количество миллисекунд с учётом точности и корректности системного таймера и планировщика. |
| 3 |
public static boolean holdsLock(Object x) Возвращает true, если и только если текущий поток удерживает блокировку монитора указанного объекта. |
| 4 |
public static Thread currentThread() Возвращает ссылку на текущий выполняющийся объект потока. |
| 5 |
public static void dumpStack() Выводит трассировку стека текущего потока в стандартный поток ошибок. |
Пример
Следующая программа ThreadClassDemo демонстрирует некоторые методы класса Thread:
Код файла DisplayMessage.java:
Код файла GuessANumber.java:
ThreadClassDemo.java код файла:
Результат выполнения следующий; результат каждый раз отличается.
Starting hello thread... Starting goodbye thread... Hello Hello Hello Hello Hello Hello Goodbye Goodbye Goodbye Goodbye Goodbye .......
Создание потока с помощью Callable и Future
1. Создайте класс реализации интерфейса Callable и реализуйте метод call(). Метод call() будет служить телом выполнения потока и имеет возвращаемое значение.
2. Создайте экземпляр класса реализации Callable и используйте класс FutureTask для обёртывания объекта Callable. Объект FutureTask инкапсулирует возвращаемое значение метода call() этого объекта Callable.
3. Используйте объект FutureTask в качестве цели объекта Thread, чтобы создать и запустить новый поток.
4. Вызовите метод get() объекта FutureTask, чтобы получить возвращаемое значение после завершения выполнения дочернего потока.
Пример
Сравнение трёх способов создания потоков
1. При создании многопоточности путём реализации интерфейсов Runnable или Callable класс потока реализует только интерфейс Runnable или Callable и всё ещё может наследовать другие классы.
2. При создании многопоточности путём наследования класса Thread код прост. Если нужно получить доступ к текущему потоку, не нужно использовать Thread.currentThread(); можно напрямую использовать this для получения текущего потока.
Несколько ключевых понятий потоков
В многопоточном программировании необходимо понимать следующие понятия:
- Синхронизация потоков
- Взаимодействие между потоками
- Взаимоблокировка потоков
- Управление потоками: приостановка, остановка и возобновление
Использование многопоточности
Ключ к эффективному использованию многопоточности — понимание того, что программы выполняются параллельно, а не последовательно. Например, если в программе есть две подсистемы, которые должны выполняться параллельно, то в этот момент необходимо многопоточное программирование.
Используя многопоточность, можно писать очень эффективные программы. Однако учтите, что если создать слишком много потоков, эффективность выполнения программы на самом деле снизится, а не повысится.
Помните, что накладные расходы на переключение контекста также важны. Если создать слишком много потоков, CPU будет тратить больше времени на переключение контекста, чем на выполнение программы!
Другие расширения