Синхронізація потоків. Оператор 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