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

Insert math as
Block
Inline
Additional settings
Formula color
Text color
#333333
Type math using LaTeX
Preview
\({}\)
Nothing to preview
Insert