Синхронізація потоків. Оператор synchronized
Завершення та переривання потоку. Синхронізація потоків
Потоки в Java працюють паралельно, але іноді потрібно керувати їх життєвим циклом: завершувати їх або переривати. Окрім того, якщо кілька потоків взаємодіють з одними й тими самими даними, може виникати проблема синхронізації, що може призвести до некоректних результатів. Java надає можливість синхронізувати потоки для запобігання таким конфліктам за допомогою ключового слова **`synchronized`**.
Завершення потоку
Потік завершується, коли метод `run()` закінчує своє виконання. Проте є випадки, коли потрібно завершити потік достроково або відреагувати на завершення. Java не надає прямого методу для зупинки потоку (через можливі проблеми, пов’язані з некоректним завершенням), але можна створювати власні механізми зупинки.
Приклад: завершення через флаг
Найчастіше використовують флаг, який повідомляє потоку, що він має зупинитися.
class StoppableThread extends Thread {
private volatile boolean running = true; // Флаг для контролю роботи потоку
public void run() {
while (running) {
System.out.println(“Потік працює…”);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(“Потік був перерваний”);
}
}
System.out.println(“Потік завершений.”);
}
public void stopThread() {
running = false;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
StoppableThread thread = new StoppableThread();
thread.start();
Thread.sleep(5000); // Чекати 5 секунд
thread.stopThread(); // Завершити потік
}
}
У прикладі вище використовується volatile флаг `running`, щоб гарантувати, що зміни цього флагу бачать всі потоки. Коли флаг встановлюється в `false`, потік завершує свою роботу.
Переривання потоку
Java надає метод interrupt(), який можна використовувати для сповіщення потоку про те, що його потрібно перервати. Це не зупиняє потік одразу, але викликає InterruptedException у випадках, коли потік знаходиться в стані очікування (наприклад, при використанні `sleep()` або `wait()`).
Приклад переривання потоку:
class InterruptibleThread extends Thread {
public void run() {
try {
for (int i = 0; i < 10; i++) {
System.out.println(“Потік працює: ” + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(“Потік був перерваний”);
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
InterruptibleThread thread = new InterruptibleThread();
thread.start();
Thread.sleep(3000); // Почекати 3 секунди
thread.interrupt(); // Перервати потік
}
}
У цьому прикладі, коли викликається метод `interrupt()`, потік викидає виняток `InterruptedException`, що дозволяє зупинити його.
Синхронізація потоків
Коли кілька потоків отримують доступ до одного ресурсу, наприклад до змінної або об’єкта, може виникнути конфлікт — так звані **гонки потоків** (race conditions). Це призводить до неправильних результатів або непередбачуваної поведінки програми.
Для вирішення таких проблем у Java використовується синхронізація.
Оператор `synchronized`
Оператор synchronized дозволяє обмежити доступ кількох потоків до критичних секцій коду, щоб уникнути одночасного виконання цих ділянок різними потоками.
Синхронізація методу
Коли метод позначений ключовим словом `synchronized`, тільки один потік може одночасно виконувати цей метод для одного екземпляра об’єкта.
class Counter {
private int count = 0;
// Синхронізований метод
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(“Значення лічильника: ” + counter.getCount());
}
}
У цьому прикладі метод `increment()` синхронізований, тому лише один потік може виконувати цей метод одночасно для даного екземпляра класу `Counter`.
Синхронізація блоку коду
Іноді потрібно синхронізувати не весь метод, а лише певну частину коду. У цьому випадку можна використовувати синхронізований блок:
class Counter {
private int count = 0;
public void increment() {
// Синхронізований блок
synchronized(this) {
count++;
}
}
public int getCount() {
return count;
}
}
synchronized(this) блокує поточний об’єкт (екземпляр класу), щоб інші потоки не могли виконувати синхронізовані блоки або методи для цього ж об’єкта.
Синхронізація на класі
Якщо ви хочете синхронізувати доступ до статичних методів або змінних, можна використовувати блок синхронізації на рівні класу:
class SharedCounter {
private static int count = 0;
// Синхронізація на рівні класу
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
}
У цьому випадку використовується `synchronized` на статичних методах, щоб забезпечити синхронізацію доступу на рівні класу.
Методи керування потоками
- wait(): Зупиняє виконання потоку до того моменту, поки інший потік не викличе метод `notify()` або `notifyAll()`.
- notify(): Відновлює виконання одного потоку, який був призупинений викликом `wait()`.
- notifyAll(): Відновлює виконання всіх потоків, що були призупинені викликом `wait()`.
Приклад використання `wait()` і `notify()`:
class Data {
private String packet;
// Метод для запису даних
public synchronized void send(String packet) {
this.packet = packet;
notify(); // Повідомляємо потік, що дані готові
}
// Метод для отримання даних
public synchronized String receive() throws InterruptedException {
wait(); // Очікуємо, поки інший потік не викличе notify()
return packet;
}
}
public class Main {
public static void main(String[] args) {
Data data = new Data();
Thread sender = new Thread(() -> {
try {
Thread.sleep(1000); // Затримка перед відправкою
data.send(“Привіт від потоку відправника!”);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread receiver = new Thread(() -> {
try {
String message = data.receive();
System.out.println(“Отримано повідомлення: ” + message);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
receiver.start();
sender.start();
}
}
У цьому прикладі потік-отримувач (receiver) чекає, поки не буде отримане повідомлення, використовуючи метод `wait()`, а потік-відправник (sender) сповіщає про готовність даних через `notify()`.
Висновок
- Потоки можна завершувати через флаги або переривати через метод `interrupt()`.
- Для безпечного доступу до спільних ресурсів кількох потоків використовується синхронізація за допомогою ключового слова synchronized.
Завершення та переривання потокуСинхронізація потоків. Оператор synchronized