Java - Взаимодействие между потоками

Добро пожаловать, стремящиеся к мастерству в Java! Сегодня мы отправляемся в захватывающее путешествие по миру взаимодействия между потоками в Java. В качестве вашего дружелюбного соседа по компьютерной науке, я здесь, чтобы провести вас через эту увлекательную тему. Так что взять свой любимый напиток, устроиться комфортно и погружайтесь вместе с нами!

Java - Inter-thread Communication

Что такое взаимодействие между потоками?

Представьте себе, что вы участвуете в эстафете. Вам нужно передать эстафету своему напарнику в самый подходящий момент. Вот и основная идея взаимодействия между потоками в мире программирования. Это как различные потоки в Java-программе общаются между собой, координируют свои действия и обмениваются информацией.

Почему взаимодействие между потоками важно?

Взаимодействие между потоками критически важно для создания эффективных, синхронизированных программ. Без него наши потоки были бы как бегуны в отдельных забегах, неспособные эффективно сотрудничать или делиться ресурсами.

Методы, используемые для взаимодействия между потоками

Java предоставляет несколько методов для взаимодействия между потоками. Давайте рассмотрим их в удобной таблице:

Метод Описание
wait() Приостанавливает текущий поток до тех пор, пока другой поток не вызовет метод notify() или notifyAll() для этого объекта
notify() Пробуждает один поток, который ожидает на мониторе этого объекта
notifyAll() Пробуждает все потоки, которые ожидают на мониторе этого объекта

Теперь разберем эти методы и посмотрим, как они работают на практике!

Метод wait()

Метод wait() похож на то, что вы говорите потоку: "Эй, отдохни, пока кто-то не покоснется тебя". Вот как он работает:

synchronized(object) {
while(condition) {
object.wait();
}
}

В этом коде:

  1. Сначала мы синхронизируемся с объектом для обеспечения безопасности потоков.
  2. Проверяем условие в цикле while.
  3. Если условие истинно, вызываем wait(), что заставляет поток подождать уведомления.

Метод notify()

Метод notify() похож на то, что вы поднимаете на плечо ожидающий поток и говорите: "Пробудись! Твой ход". Вот как мы его используем:

synchronized(object) {
// Изменяем условие
condition = true;
object.notify();
}

В этом коде:

  1. Мы синхронизируемся с тем же объектом, что и в вызове wait().
  2. Изменяем условие, которое проверял ожидающий поток.
  3. Вызываем notify(), чтобы пробудить один ожидающий поток.

Метод notifyAll()

Метод notifyAll() похож на крик: "Всем проснитесь!". Он используется, когда вы хотите уведомить все ожидающие потоки. Вот пример:

synchronized(object) {
// Изменяем условие
condition = true;
object.notifyAll();
}

Этот метод работает аналогично notify(), но пробуждает все ожидающие потоки, а не только один.

Реальный пример: Проблема производителя-потребителя

Поместим все это в контекст классического примера: проблема производителя-потребителя. Представьте себе пекарню, где один человек (производитель) печет хлеб, а другой (потребитель) продает его. Они делятся ограниченным местом на полке.

Вот как мы можем реализовать это в Java:

class Bakery {
private int breads = 0;
private final int CAPACITY = 5;

public synchronized void produceBread() throws InterruptedException {
while (breads == CAPACITY) {
System.out.println("Shelf is full! Baker is waiting...");
wait();
}
breads++;
System.out.println("Baker made a bread. Total: " + breads);
notify();
}

public synchronized void sellBread() throws InterruptedException {
while (breads == 0) {
System.out.println("No bread! Seller is waiting...");
wait();
}
breads--;
System.out.println("Sold a bread. Remaining: " + breads);
notify();
}
}

class Baker implements Runnable {
private Bakery bakery;

public Baker(Bakery bakery) {
this.bakery = bakery;
}

public void run() {
for (int i = 0; i < 10; i++) {
try {
bakery.produceBread();
Thread.sleep(1000); // Время для печения
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class Seller implements Runnable {
private Bakery bakery;

public Seller(Bakery bakery) {
this.bakery = bakery;
}

public void run() {
for (int i = 0; i < 10; i++) {
try {
bakery.sellBread();
Thread.sleep(1500); // Время между продажами
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class BakeryDemo {
public static void main(String[] args) {
Bakery bakery = new Bakery();
Thread baker = new Thread(new Baker(bakery));
Thread seller = new Thread(new Seller(bakery));

baker.start();
seller.start();
}
}

Разберем это:

  1. У нас есть класс Bakery, который управляет запасом хлеба.
  2. Метод produceBread() представляет пекаря, который печет хлеб. Если полка полна, пекарь ждет.
  3. Метод sellBread() представляет продавца, который продает хлеб. Если нет хлеба, продавец ждет.
  4. Мы используем wait(), когда условия не благоприятны для производства или продажи.
  5. Мы используем notify() после производства или продажи, чтобы уведомить другой поток.
  6. Классы Baker и Seller работают в отдельных потоках, постоянно пытаясь произвести или продать хлеб.

Когда вы запустите эту программу, вы увидите, как пекарь и продавец работают вместе, ждут, когда необходимо, и уведомляют друг друга, когда могут продолжить. Это как наблюдать за хорошо скоординированным танцем!

Заключение

Итак, друзья, мы прошли через землю взаимодействия между потоками в Java. Мы увидели, как потоки могут координировать свои действия с помощью wait(), notify() и notifyAll(). Мы даже создали виртуальную пекарню, чтобы продемонстрировать эти концепции на практике!

Помните, как в нашем примере с пекарней, хорошее взаимодействие между потоками - это все о балансе и координации. Это знание того, когда работать, когда ждать и когда уведомлять других. Овладев этим, вы будете уже на полпути к созданию эффективных, хорошо скоординированных Java-программ.

Успехов в практике, оставайтесь любознательными и счастливого кодирования! И помните, как в программировании, так и в жизни, хорошая коммуникация - ключ к успеху. До встречи в следующий раз, это ваш дружелюбный сосед по компьютерной науке, подписываюсь!

Credits: Image by storyset