В предыдущей статье мы обсудили ключевые концепции многопоточности, ее преимущества и основные проблемы. В этой статье мы сосредоточимся на практическом применении потоков в Java с использованием классов Thread
и Runnable
.
Дальнейшие статьи будут посвящены более сложным аспектам многопоточности. Вот список тем, которые мы планируем рассмотреть:
- Введение в многопоточность: Преимущества, проблемы и ключевые концепции.
- Потоки в Java: Thread, Runnable. 📍 Вы здесь.
- Синхронизация потоков: Использование synchronized, блокировок и ключевого слова volatile.
- Locks и другие механизмы синхронизации: Semaphore, CountDownLatch, ReentrantLock.
- Пул потоков и управление задачами: Executor и ExecutorService.
- Асинхронные задачи и фьючерсы: CompletableFuture.
- Fork/Join Framework: Разделение задач для параллелизма.
- Виртуальные потоки: Новое поколение потоков в Java.
- Реактивное программирование: Асинхронность на новом уровне.
Архитектура потоков в Java
Каждый поток в Java представлен объектом класса Thread
. Хотя управление потоками осуществляется внутри JVM, фактически потоки работают на уровне операционной системы. Такие потоки называются платформенными.
Когда запускается платформенный поток, JVM передаёт управление этим потоком операционной системе, которая выделяет необходимые ресурсы (например, процессорное время) и начинает его выполнение. Этот механизм называется многопоточностью на уровне ОС (OS-level threads). Количество потоков, которые могут выполняться одновременно, зависит от возможностей операционной системы и числа ядер процессора.
Каждый поток выполняется в своём собственном контексте, имея отдельный стек вызовов и локальные переменные. JVM координирует выполнение потоков, распределяя им квант времени и переключая контексты, чтобы обеспечить параллельность. Поскольку потоки одного процесса разделяют общую память, важно правильно синхронизировать их взаимодействие, чтобы избежать проблем, таких как состояние гонки. Этой теме будет посвящена следующая статья.
Важно отметить, что сама JVM является многопоточной. Она использует внутренние системные потоки для выполнения таких задач, как сборка мусора (Garbage Collection) и другие фоновые операции, которые работают параллельно с потоками приложения и обеспечивают стабильность работы программы.
Ограничения многопоточности
Несмотря на возможность создавать множество потоков, чрезмерное их использование может негативно сказаться на производительности.
Создание потоков требует памяти для стека вызовов и процессорного времени. С увеличением количества потоков возрастает потребление ресурсов, что может привести к перегрузке системы.
Операционная система вынуждена часто переключать контекст между потоками, что создаёт накладные расходы. В результате часть процессорного времени уходит на управление потоками, а не на выполнение полезных задач, что замедляет работу программы.
Виртуальные потоки
Традиционные (платформенные) потоки накладывают ограничения на количество потоков, которые могут выполняться одновременно. Их создание и переключение контекста требуют значительных системных ресурсов.
Чтобы решить эту проблему, начиная с Java 19, была введена экспериментальная функция — виртуальные потоки (virtual threads) в рамках проекта Loom. Виртуальные потоки позволяют создавать тысячи и даже миллионы лёгковесных потоков, которые управляются JVM, а не напрямую операционной системой. Это значительно уменьшает накладные расходы и улучшает масштабируемость многопоточных приложений.
В Java 21, выпущенной в сентябре 2023 года, виртуальные потоки стали стандартной функцией. Поскольку виртуальные потоки представляют собой важное нововведение, они заслуживают отдельного рассмотрения. В этой статье мы сосредоточимся на традиционных потоках, а возможности и преимущества виртуальных потоков будут рассмотрены в следующих материалах.
Создание потока
В Java всё является объектами, и потоки — не исключение. Класс java.lang.Thread
— это основной инструмент для создания и управления потоками. Например, для создания и запуска 10 потоков необходимо создать 10 объектов этого класса.
Существует несколько способов создания платформенного потока:
- Наследование от класса
Thread
и переопределение методаrun()
. - Реализация интерфейса
Runnable
и передача его экземпляра в конструкторThread
.
Способ 1: Наследование от класса Thread
class MyThread extends Thread {
public void run() {
// Код для выполнения в отдельном потоке
System.out.println("Поток " + getName() + " начал выполнение.");
}
}
public class Main {
public static void main(String[] args) {
// Создание нового потока
MyThread myThread = new MyThread();
// Запуск нового потока
myThread.start();
System.out.println("Основной поток продолжает выполнение.");
}
}
В этом примере метод run()
содержит код, который будет выполняться в отдельном потоке.
Thread
накладывает ограничение: класс может наследоваться только от одного суперкласса. Это может стать проблемой, если ваш класс уже наследует другой класс.Способ 2: Использование интерфейса Runnable
Часто более предпочтительным является использование интерфейса Runnable
. Он содержит метод run()
, который вы должны реализовать, а затем передать экземпляр этого класса в конструктор Thread
.
class MyRunnable implements Runnable {
public void run() {
// Код для выполнения в этом потоке
}
}
// Создание и запуск нового потока
Thread thread = new Thread(new MyRunnable());
thread.start();
Использование лямбда-выражений
Начиная с Java 8, вы можете использовать лямбда-выражения для упрощения кода:
Thread thread = new Thread(() -> {
// Код для выполнения в этом потоке
});
thread.start();
Это особенно удобно для создания простых потоков без необходимости создания отдельного класса.
Разница между start()
и run()
Важно понимать различие между вызовами методов start()
и run()
:
start()
— метод класса Thread
, который создает новый поток и вызывает метод run()
в этом новом потоке. Вызов start()
инициирует параллельное выполнение потока.
run()
— это метод, содержащий код, который будет выполнен в потоке. Если вызвать run()
напрямую, без вызова start()
, код будет выполнен в текущем потоке, как обычный метод, и новый поток не будет создан.
public class ThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Выполняется в новом потоке");
});
// Неправильный способ — вызов run() напрямую
// thread.run(); // Это выполнится в текущем потоке
// Правильный способ — вызов start()
thread.start();
}
}
Управление потоками
После создания потока с помощью класса Thread
управление его выполнением осуществляется через несколько ключевых методов:
start()
: Запускает поток. Важно помнить, что именно вызовstart()
приводит к тому, что JVM создаёт новый поток выполнения и вызывает методrun()
этого потока.join()
: Заставляет текущий поток дождаться завершения другого потока, у которого вызван этот метод, прежде чем продолжить выполнение программы. Это полезно, когда нужно убедиться, что определённый поток завершил свою работу перед продолжением.sleep()
: Приостанавливает выполнение текущего потока на заданный промежуток времени, освобождая ресурсы процессора для других потоков. После истечения указанного времени поток продолжит выполнение.
Рассмотрим следующий код:
package dev.struchkov.example.threads.first;
public class SimpleThreadMain {
public static void main(String[] args) {
final Runnable oneThread = new SimpleThread("Первый");
final Runnable twoThread = new SimpleThread("Второй");
final Runnable threeThread = new SimpleThread("Третий");
new Thread(oneThread).start();
new Thread(twoThread).start();
new Thread(threeThread).start();
System.out.println("\nSurprise");
}
}
Когда будет выведено сообщение "Surprise"?
Сообщение “Surprise” будет выведено почти сразу после запуска трёх потоков, чаще всего перед выводом сообщений из других потоков. Это связано с тем, что после вызова метода start()
, потоки переходят в состояние готовности к выполнению (RUNNABLE), но их фактический запуск и выполнение зависят от планировщика потоков операционной системы. В то же время, основной поток (в котором выполняется метод main
) продолжает своё выполнение без задержек и достигает вызова System.out.println("\nSurprise")
раньше, чем новые потоки начнут выводить свои сообщения.
Метод join()
Метод join()
часто используется, когда необходимо синхронизировать выполнение потоков, обеспечивая выполнение одного потока только после завершения другого. Рассмотрим пример:
public class ThreadJoinExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println("Thread 1 is running");
try {
Thread.sleep(1000); // Имитация работы
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 has finished");
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2 is running");
try {
Thread.sleep(2000); // Имитация работы
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2 has finished");
});
thread1.start();
thread2.start();
try {
thread1.join(); // Ожидаем завершения thread1
thread2.join(); // Ожидаем завершения thread2
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Both threads have finished. Main thread resumes.");
}
}
Мы создаём и запускаем два потока thread1
и thread2
. Каждый из них выводит сообщение о начале и завершении своей работы, с имитацией некоторой работы с помощью Thread.sleep()
.
После запуска потоков основной поток (main
) вызывает thread1.join()
и thread2.join()
. Это означает, что основной поток будет ожидать завершения thread1
и thread2
перед выполнением следующей строки.
Результат выполнения:
Thread 1 is running
Thread 2 is running
Thread 1 has finished
Thread 2 has finished
Both threads have finished. Main thread resumes.
Обратите внимание, что thread1
завершается раньше, так как sleep(1000)
меньше, чем sleep(2000)
. Однако основной поток не выводит сообщение до тех пор, пока оба потока не завершат свою работу.
Используя join()
, вы обеспечиваете, что данные, которые зависят от результатов работы потоков, не будут использованы до их готовности.
Жизненный цикл потока
Каждый поток в Java проходит несколько состояний в своём жизненном цикле, начиная с создания и заканчивая завершением. Эти состояния контролируются как JVM, так и операционной системой. Понимание жизненного цикла потока важно для корректного управления многопоточными приложениями и предотвращения ошибок, связанных с синхронизацией и управлением ресурсами.
NEW. Поток создан, но ещё не запущен. Это состояние возникает сразу после создания объекта Thread
, но до вызова метода start()
. В этом состоянии поток не готов к выполнению и не потребляет системные ресурсы.
Thread thread = new Thread(new OrderTask()); // Новый поток
RUNNABLE. После вызова метода start()
поток переходит в состояние RUNNABLE. В этом состоянии поток считается готовым к выполнению и может быть выбран планировщиком потоков для выполнения на процессоре. Поток может фактически выполняться или ожидать своей очереди на выполнение.
thread.start(); // Поток становится готовым к выполнению
BLOCKED. Поток переходит в это состояние, если он пытается войти в синхронизированный блок или метод, доступ к которому в данный момент удерживается другим потоком. Как только монитор освобождается, поток возвращается в состояние RUNNABLE.
Подробнее о блокировках и мониторах будет рассмотрено в следующей статье.
WAITING. Поток находится в состоянии ожидания без указания времени, пока другой поток не разбудит его. Это происходит при вызове методов Object.wait()
, Thread.join()
без таймаута или LockSupport.park()
. Поток остаётся в этом состоянии, пока не получит уведомление или пока другой поток не завершится (в случае join()
).
thread.join(); // Ожидание завершения другого потока
TIMED_WAITING. Похожее на состояние WAITING, но с указанным временем ожидания. Поток переходит в это состояние при вызове методов sleep()
или join()
с указанием таймаута. По истечении времени ожидания поток автоматически возвращается в состояние RUNNABLE.
Thread.sleep(1000); // Ожидание в течение 1 секунды
TERMINATED. Поток переходит в это состояние, когда метод run()
завершает своё выполнение либо из-за нормального завершения, либо из-за неперехваченного исключения. В этом состоянии поток больше не может быть перезапущен.
public void run() {
System.out.println("Processing order...");
// Завершение работы потока
}
Попытка вызвать start()
более одного раза на одном и том же экземпляре потока приведет к исключению IllegalThreadStateException
.
Thread thread = new Thread(task);
thread.start();
thread.start(); // Throws IllegalThreadStateException
Пример жизненного цикла потока
Рассмотрим пример, иллюстрирующий различные состояния потока:
public class ThreadLifeCycleDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Мы находимся в методе 'run'.");
});
System.out.println("Состояние потока после создания: " + thread.getState()); // NEW
thread.start();
System.out.println("Состояние потока после вызова start(): " + thread.getState()); // RUNNABLE
// Дадим потоку время для завершения
try {
Thread.sleep(100); // Переход основного потока в TIMED_WAITING
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Состояние потока после завершения: " + thread.getState()); // TERMINATED
}
}
- NEW: При создании нового объекта
Thread
, но до вызоваstart()
, поток находится в состоянии NEW. - RUNNABLE: После вызова
start()
поток переходит в состояние RUNNABLE. Он может выполняться немедленно или ждать своей очереди на выполнение. - TIMED_WAITING: Когда основной поток вызывает
Thread.sleep(100)
, он сам переходит в состояниеTIMED_WAITING
на 100 миллисекунд, позволяя другому потоку завершить работу. - TERMINATED: После завершения метода
run()
поток переходит в состояниеTERMINATED
Daemon threads
Демон-поток — это специальный тип потока, который выполняет фоновые задачи и не блокирует завершение программы. В отличие от обычных (пользовательских) потоков, демон-потоки автоматически завершаются, как только все пользовательские потоки программы завершили свою работу. Это означает, что JVM не будет ждать завершения демон-потоков при завершении приложения.
Демон-потоки полезны для выполнения задач, которые должны работать в фоновом режиме, таких как мониторинг ресурсов системы, сборка мусора или выполнение периодических очисток.
Чтобы указать, что поток является демоном, необходимо вызвать метод setDaemon(true)
до запуска потока с помощью метода start()
:
public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("Демон-поток работает...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
// Устанавливаем поток как демон
daemonThread.setDaemon(true);
daemonThread.start();
// Основной поток завершает свою работу через 2 секунды
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Основной поток завершён.");
}
}
Метод setDaemon(true)
помечает поток как демон. Это должно быть сделано до вызова start()
. Если попытаться установить статус демона после запуска потока, будет выброшено исключение IllegalThreadStateException
.
Демон-поток выполняет бесконечный цикл, выводя сообщение каждые 500 миллисекунд. Основной поток (main) спит 2 секунды, затем выводит сообщение и завершается.
Демон-поток работает...
Демон-поток работает...
Демон-поток работает...
Демон-поток работает...
Основной поток завершён.
После завершения основного потока демон-поток автоматически прекращает свою работу, и программа завершается. Вы можете заметить, что демон-поток может не завершить все свои итерации цикла перед завершением программы.
Приоритеты потоков
Каждый поток имеет приоритет, который влияет на то, как JVM распределяет время процессора между потоками. Приоритеты потоков служат подсказкой для планировщика потоков JVM при принятии решения о том, какой поток выполнить следующим. Однако окончательное решение о распределении времени процессора зависит от реализации JVM и операционной системы.
Потоки с более высоким приоритетом, как правило, получают больше процессорного времени, но это не гарантирует, что они будут всегда выполняться раньше потоков с более низким приоритетом. Приоритеты лишь помогают JVM принимать решение, но не являются обязательными для исполнения.
Приоритеты потоков задаются числами от 1 до 10:
- MIN_PRIORITY (1): Минимальный приоритет.
- NORM_PRIORITY (5): Нормальный приоритет, который используется по умолчанию.
- MAX_PRIORITY (10): Максимальный приоритет.
Приоритет потока можно установить с помощью метода setPriority(int priority)
. Этот метод должен быть вызван после создания потока, но до его запуска методом start()
.
public class ThreadPriorityExample {
public static void main(String[] args) {
Thread lowPriorityThread = new Thread(() -> {
System.out.println("Низкоприоритетный поток начал работу.");
});
Thread highPriorityThread = new Thread(() -> {
System.out.println("Высокоприоритетный поток начал работу.");
});
lowPriorityThread.setPriority(Thread.MIN_PRIORITY); // Приоритет 1
highPriorityThread.setPriority(Thread.MAX_PRIORITY); // Приоритет 10
lowPriorityThread.start();
highPriorityThread.start();
}
}
Вы можете ожидать, что highPriorityThread
всегда будет выполняться перед lowPriorityThread
, но на практике это не гарантируется. Результат может варьироваться при каждом запуске программы.
При создании нового потока он наследует приоритет потока, который его создал. Например, если поток с приоритетом 7 создаёт новый поток, то этот новый поток также будет иметь приоритет 7, если явно не указано иное.
Установка максимального приоритета для большого количества потоков может привести к ухудшению производительности, так как JVM будет тратить больше времени на планирование задач, пытаясь распределить ресурсы между потоками с одинаково высоким приоритетом.
В большинстве случаев программы должны быть спроектированы так, чтобы корректно работать без необходимости вручную управлять приоритетами потоков. Полагаться на приоритеты для обеспечения корректности работы программы не рекомендуется.
Thread Interruption
Прерывание потоков — это механизм, с помощью которого один поток может сигнализировать другому о необходимости остановить выполнение. Однако важно помнить, что прерывание не останавливает поток немедленно. Это лишь сигнал, который поток может обработать, и сам по себе он не заставляет поток остановиться.
В Java прерывание реализовано с помощью метода interrupt()
класса Thread
. Этот метод устанавливает флаг прерывания для потока, сигнализируя, что поток был прерван. Однако поток должен сам проверить этот флаг и принять решение о завершении работы.
Методы для работы с прерываниями:
interrupt()
: Отправляет сигнал прерывания потоку, устанавливая флаг прерывания.isInterrupted()
: Проверяет, был ли поток прерван, не сбрасывая флаг прерывания.interrupted()
: Статический метод, который проверяет флаг прерывания текущего потока и сбрасывает его.
Прерывание потока с блокирующими операциями
Если поток прерывается во время выполнения блокирующих операций, таких как sleep()
, wait()
или join()
, выбрасывается исключение InterruptedException
. В таком случае важно корректно обработать это исключение, чтобы завершить поток безопасно.
public class BlockingThreadExample {
public static void main(String[] args) {
Thread blockingThread = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
System.out.println("Блокирующий поток работает... " + i);
// Поток засыпает на 1 секунду
Thread.sleep(1000);
}
} catch (InterruptedException e) {
// Поток прерван во время блокирующей операции
System.out.println("Блокирующий поток был прерван.");
return; // Завершаем работу потока
}
System.out.println("Блокирующий поток завершил свою работу.");
});
blockingThread.start();
// Прерывание потока через 3 секунды
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
blockingThread.interrupt(); // Прерывание потока
}
}
- Поток выполняет цикл, в котором засыпает на 1 секунду с помощью
Thread.sleep(1000)
. - Через 3 секунды главный поток вызывает
blockingThread.interrupt()
, прерывая блокирующий поток. - Когда
sleep()
обнаруживает, что поток был прерван, выбрасываетсяInterruptedException
, который мы обрабатываем в блокеcatch
, корректно завершая работу потока.
Если вы перехватываете InterruptedException
, но не хотите завершать поток, можно восстановить флаг прерывания:
try {
// Блокирующая операция
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Восстанавливаем флаг прерывания
}
Прерывание потока без блокирующих операций
Если поток не выполняет блокирующие операции, прерывание не вызовет исключение. В таком случае поток должен сам регулярно проверять флаг прерывания с помощью метода isInterrupted()
, чтобы корректно завершить выполнение.
public class NonBlockingThreadExample {
public static void main(String[] args) {
Thread nonBlockingThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
// Проверяем флаг прерывания
if (Thread.currentThread().isInterrupted()) {
System.out.println("Неблокирующий поток был прерван.");
return; // Корректно завершаем работу потока
}
System.out.println("Неблокирующий поток работает... " + i);
}
System.out.println("Неблокирующий поток завершил свою работу.");
});
nonBlockingThread.start();
// Прерывание потока через 2 секунды
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
nonBlockingThread.interrupt(); // Прерывание потока
}
}
- Поток регулярно проверяет, был ли он прерван, вызывая
isInterrupted()
. - Если флаг установлен, поток выводит сообщение и завершает работу.
- Если внутри цикла используется
Thread.sleep()
, может быть выброшеноInterruptedException
, которое также нужно обработать.
Обработка исключений
Когда в потоке происходит необработанное исключение, этот поток завершается с ошибкой. Однако это не приводит к остановке всей программы, если другие потоки продолжают выполнение. Игнорирование таких исключений может привести к непредсказуемому поведению приложения, трудностям в отладке и потенциальным утечкам ресурсов.
Обработка исключений в методе run()
Наилучшей практикой является обработка исключений внутри метода run() потока. Это позволяет локально перехватывать и обрабатывать ошибки, обеспечивая стабильность приложения.
public class ExceptionHandlingInThread {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
// Имитация ошибки
throw new RuntimeException("Ошибка в потоке");
} catch (RuntimeException e) {
System.out.println("Исключение было обработано: " + e.getMessage());
}
});
thread.start();
}
}
В этом примере исключение внутри потока перехватывается и обрабатывается в блоке try-catch
, предотвращая неконтролируемое завершение потока и обеспечивая возможность корректной реакции на ошибку.
Использование UncaughtExceptionHandler
Если исключение не обработано внутри потока, оно может быть перехвачено с помощью интерфейса UncaughtExceptionHandler
. Этот обработчик позволяет централизованно обрабатывать необработанные исключения в потоках, что особенно полезно для логирования ошибок или выполнения действий по восстановлению.
Чтобы установить глобальный обработчик для конкретного потока, используется метод setUncaughtExceptionHandler()
.
public class UncaughtExceptionHandling {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
// Имитация ошибки, без обработки try-catch
throw new RuntimeException("Необработанная ошибка в потоке");
});
// Устанавливаем обработчик необработанных исключений
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println("Поток " + t.getName() + " завершился с исключением: " + e.getMessage());
});
thread.start();
}
}
В этом примере исключение, которое не было обработано внутри потока, перехватывается UncaughtExceptionHandler
, что позволяет зарегистрировать факт ошибки и предпринять необходимые действия.
Установка глобального обработчика исключений
Кроме установки обработчика для конкретного потока, можно задать глобальный обработчик для всех потоков через метод Thread.setDefaultUncaughtExceptionHandler()
:
public class GlobalUncaughtExceptionHandling {
public static void main(String[] args) {
// Устанавливаем глобальный обработчик необработанных исключений
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.out.println("Поток " + t.getName() + " завершился с исключением: " + e.getMessage());
});
Thread thread = new Thread(() -> {
// Имитация ошибки без обработки try-catch
throw new RuntimeException("Необработанная ошибка в потоке");
});
thread.start();
}
}
Теперь все потоки, в которых возникнут необработанные исключения, будут использовать этот глобальный обработчик.
Заключение
Многопоточность в Java — мощный инструмент для создания параллельных и высокопроизводительных приложений, позволяющий улучшить отзывчивость и эффективность программ. В этой статье мы рассмотрели ключевые компоненты многопоточности, включая класс Thread
и интерфейс Runnable
, которые обеспечивают гибкость при создании и управлении потоками.
Мы подробно обсудили жизненный цикл потока и методы управления его состояниями, такие как start()
, join()
и sleep()
, которые играют важную роль в синхронизации и координации выполнения задач. Также мы рассмотрели демон-потоки, которые полезны для выполнения фоновых задач без блокировки завершения основной программы.
Правильная обработка исключений в многопоточной среде особенно важна для обеспечения стабильности и надёжности приложений. Исключения, возникающие внутри потоков, требуют локальной обработки с использованием блоков try-catch
. В случае необработанных исключений использование UncaughtExceptionHandler
позволяет централизованно управлять ошибками, которые иначе могли бы остаться незамеченными.
Мы также затронули темы прерывания потоков и приоритетов, подчёркивая важность осознанного подхода к управлению потоками. Эффективное использование многопоточности требует тщательного планирования архитектуры приложений для предотвращения проблем с синхронизацией, состояний гонки и повышения стабильности системы.
В следующей статье мы обсудим синхронизацию потоков: использование ключевого слова synchronized
, блокировок (Lock) и ключевого слова volatile
, которые являются критически важными для обеспечения корректной работы многопоточных приложений.