1. Введение
В этом уроке мы рассмотрим, как мы можем печатать четные и нечетные числа, используя два потока.
Цель состоит в том, чтобы напечатать числа по порядку, в то время как один поток печатает только четные числа, а другой поток печатает только нечетные числа. Для решения проблемы мы будем использовать концепции синхронизации потоков и межпотокового взаимодействия.
2. Потоки в Java
Потоки — это легкие процессы, которые могут выполняться одновременно. Параллельное выполнение нескольких потоков может быть хорошим с точки зрения производительности и использования ЦП, поскольку мы можем работать над более чем одной задачей одновременно через разные потоки, работающие параллельно.
Более подробную информацию о потоках в Java можно найти в этой статье .
В Java мы можем создать поток, либо расширив класс Thread
, либо реализовав интерфейс Runnable
. В обоих случаях мы переопределяем метод run
и пишем в нем реализацию потока.
Дополнительную информацию о том, как использовать эти методы для создания треда, можно найти здесь .
3. Синхронизация потоков
В многопоточной среде возможно, что 2 или более потоков обращаются к одному и тому же ресурсу примерно в одно и то же время. Это может быть фатальным и привести к ошибочным результатам. Чтобы предотвратить это, нам нужно убедиться, что только один поток обращается к ресурсу в данный момент времени.
Мы можем добиться этого, используя синхронизацию потоков.
В Java мы можем пометить метод или блок как синхронизированный, что означает, что только один поток сможет войти в этот метод или блок в данный момент времени.
Более подробную информацию о синхронизации потоков в Java можно найти здесь .
4. Межпоточная связь
Взаимодействие между потоками позволяет синхронизированным потокам взаимодействовать друг с другом, используя набор методов.
Используемые методы — это wait
, notify
и notifyAll,
которые унаследованы от класса Object .
Функция Wait()
заставляет текущий поток бесконечно ждать, пока какой-либо другой поток не вызовет notify() или notifyAll()
для того же объекта. Мы можем вызвать notify()
для пробуждения потоков, ожидающих доступа к монитору этого объекта.
Подробнее о работе этих методов можно узнать здесь .
5. Альтернативная печать нечетных и четных чисел
5.1. Использование wait()
и notify()
Мы будем использовать обсуждаемые концепции синхронизации и взаимодействия между потоками для вывода нечетных и четных чисел в порядке возрастания с использованием двух разных потоков.
На первом этапе мы реализуем интерфейс Runnable
для определения логики обоих потоков . В методе run
мы проверяем, является ли число четным или нечетным.
Если число четное, мы вызываем метод printEven класса
Printer
, иначе мы вызываем метод printOdd
:
class TaskEvenOdd implements Runnable {
private int max;
private Printer print;
private boolean isEvenNumber;
// standard constructors
@Override
public void run() {
int number = isEvenNumber ? 2 : 1;
while (number <= max) {
if (isEvenNumber) {
print.printEven(number);
} else {
print.printOdd(number);
}
number += 2;
}
}
}
Мы определяем класс Printer следующим образом:
class Printer {
private volatile boolean isOdd;
synchronized void printEven(int number) {
while (!isOdd) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println(Thread.currentThread().getName() + ":" + number);
isOdd = false;
notify();
}
synchronized void printOdd(int number) {
while (isOdd) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println(Thread.currentThread().getName() + ":" + number);
isOdd = true;
notify();
}
}
В основном методе мы используем определенный класс для создания двух потоков. Создаем объект класса Printer
и передаем его в качестве параметра конструктору TaskEvenOdd
:
public static void main(String... args) {
Printer print = new Printer();
Thread t1 = new Thread(new TaskEvenOdd(print, 10, false),"Odd");
Thread t2 = new Thread(new TaskEvenOdd(print, 10, true),"Even");
t1.start();
t2.start();
}
Первый поток будет нечетным, поэтому мы передаем false
в качестве значения параметра isEvenNumber
. Вместо этого для второго потока мы передаем true .
Мы устанавливаем maxValue равным
10 для обоих потоков, чтобы печатались только числа от 1 до 10.
Затем мы запускаем оба потока, вызывая метод start()
. Это вызовет метод run()
обоих потоков, как определено выше, в котором мы проверяем, является ли число нечетным или четным, и печатаем их.
Когда нечетный поток запустится, значение переменной number
будет равно 1. Поскольку оно меньше maxValue,
а флаг isEvenNumber
равен false, вызывается printOdd()
. В методе мы проверяем, является ли флаг isOdd
истинным, и пока он истинен, мы вызываем wait().
Поскольку изначально isOdd
имеет значение false, функция wait()
не вызывается, а значение печатается.
Затем мы устанавливаем значение isOdd
в true, чтобы нечетный поток переходил в состояние ожидания и вызывал notify()
для пробуждения четного потока. Затем четный поток просыпается и печатает четное число, поскольку флаг нечетности
равен false. Затем он вызывает notify()
, чтобы разбудить нечетный поток.
Тот же процесс выполняется до тех пор, пока значение переменной number
не станет больше, чем maxValue
.
5.2. Использование семафоров
Семафор управляет доступом к общему ресурсу с помощью счетчика. Если счетчик больше нуля, то доступ разрешен . Если он равен нулю, то доступ запрещен.
Java предоставляет класс Semaphore в пакете
java.util.concurrent
, и мы можем использовать его для реализации описанного механизма. Подробнее о семафорах можно узнать здесь .
Мы создаем два потока, нечетный поток и четный поток. Нечетный поток будет печатать нечетные числа, начиная с 1, а четный поток будет печатать четные числа, начиная с 2.
Оба потока имеют объект класса SharedPrinter
. Класс SharedPrinter
будет иметь два семафора, semOdd
и semEven
, которые будут иметь разрешения 1 и 0 для начала с . Это гарантирует, что нечетное число будет напечатано первым.
У нас есть два метода printEvenNum()
и printOddNum().
Нечетный поток вызывает метод printOddNum()
, а четный поток вызывает метод printEvenNum()
.
Чтобы напечатать нечетное число, для semOdd вызывается методAcquire
()
, и, поскольку начальное разрешение равно 1, он успешно получает доступ, печатает нечетное число и вызывает release()
для semEven.
``
Вызов release()
увеличит разрешение на 1 для semEven
, после чего четный поток сможет успешно получить доступ и напечатать четное число.
Это код для рабочего процесса, описанного выше:
public static void main(String[] args) {
SharedPrinter sp = new SharedPrinter();
Thread odd = new Thread(new Odd(sp, 10),"Odd");
Thread even = new Thread(new Even(sp, 10),"Even");
odd.start();
even.start();
}
class SharedPrinter {
private Semaphore semEven = new Semaphore(0);
private Semaphore semOdd = new Semaphore(1);
void printEvenNum(int num) {
try {
semEven.acquire();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + num);
semOdd.release();
}
void printOddNum(int num) {
try {
semOdd.acquire();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + num);
semEven.release();
}
}
class Even implements Runnable {
private SharedPrinter sp;
private int max;
// standard constructor
@Override
public void run() {
for (int i = 2; i <= max; i = i + 2) {
sp.printEvenNum(i);
}
}
}
class Odd implements Runnable {
private SharedPrinter sp;
private int max;
// standard constructors
@Override
public void run() {
for (int i = 1; i <= max; i = i + 2) {
sp.printOddNum(i);
}
}
}
6. Заключение
В этом уроке мы рассмотрели, как мы можем печатать нечетные и четные числа поочередно, используя два потока в Java. Мы рассмотрели два метода достижения одинаковых результатов: использование wait()
и notify()
и использование семафора
.
И, как всегда, полный рабочий код доступен на GitHub .