Я работаю над небольшой проблемой, когда мне нужно печатать числа последовательно с двумя потоками попеременно. Например, поток 1 печатает 1, поток 2 печатает 2, поток 1 печатает 3 и так далее...
Итак, я создал фрагмент кода ниже, но в какой-то момент оба потока переходят в состояние ожидания, и на консоли ничего не печатается.
import java.util.concurrent.atomic.AtomicInteger;
public class MultiPrintSequence {
public static void main(String[] args) {
AtomicInteger integer=new AtomicInteger(0);
Sequence sequence1=new Sequence(integer);
Sequence sequence2=new Sequence(integer);
sequence1.start();
sequence2.start();
}
}
class Sequence extends Thread{
private AtomicInteger integer;
boolean flag=false;
public Sequence(AtomicInteger integer) {
this.integer=integer;
}
@Override
public void run() {
while(true) {
synchronized (integer) {
while (flag) {
flag=false;
try {
System.out.println(Thread.currentThread().getName()+" waiting");
integer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" "+integer.incrementAndGet());
flag = true;
System.out.println(Thread.currentThread().getName()+" notifying");
integer.notify();
}
}
}
}
Наблюдая за выводом консоли, я заметил, что в какой-то момент, когда один из потоков уведомляет, другой поток в конечном итоге запускается даже до того, как уведомляющий поток переходит в состояние ожидания, и, следовательно, в какой-то момент оба потока переходят в состояние ожидания. Ниже приведена небольшая часть вывода консоли.
Thread-1 510
Thread-1 notifying
Thread-1 waiting
Thread-0 511
Thread-0 notifying
Thread-0 waiting
Thread-1 512
Thread-1 notifying
Thread-1 waiting
**Thread-0 513
Thread-0 notifying
Thread-1 514
Thread-1 notifying
Thread-1 waiting
Thread-0 waiting**
На самом деле, у меня только что возникла проблема: да, мы можем сделать флаг доступным для обоих потоков. Но это не проблема. У меня была плохая структура блока while. блок while(true) должен быть внутри блока synchronized и заменить while(flag) на if и все готово. Это был серьезный промах. Тем не менее, код будет работать и с вашим решением, спасибо :)
ИМО, вам нужно больше думать о том, что означает flag. Оба ваших потока выполняют один и тот же код, и оба они получают один и тот же аргумент. Они идентичны. Но они должны быть разными. Один из них должен печатать только нечетные числа, а другой должен печатать только четные числа. Вам нужен способ сообщить потоку: «Ваша задача — печатать только четные числа» (или «нечетные»); и вам нужен способ, чтобы каждый поток ждал, пока не придет время печатать четное/нечетное число.
P.S. В качестве дополнительного балла узнайте, почему эта проблема почти полностью противоположна той, для решения которой предназначены потоки. (Подсказка: все, что связано с потоками, связано со словом «одновременно».)




Рассмотрим эту неудачную последовательность событий. Thread1 увеличивает значение, устанавливает флаг в true и уведомляет все потоки в ожидании блокировки. Теперь Thread0 уже был в наборе ожидания. Затем просыпается Thread0, и его флаг = false. Затем Thread0 выходит из цикла while, печатает увеличенное значение и уведомляет все ожидающие потоки. Затем он переходит к следующей итерации в цикле while и вызывает ожидание объекта блокировки. Но Thread1 не находится в состоянии ожидания, скорее, планировщик отключает его от ЦП после завершения его синхронизированного блока, чтобы дать шанс Thread0. Поток 1 находится в рабочем состоянии, и планировщик снова дает ему шанс вернуться, поскольку не осталось ни одного рабочего потока. Затем Tread1 входит в цикл while, поскольку флаг = true, и вызывает ожидание того же объекта блокировки. Теперь оба потока находятся в состоянии ожидания, и их некому разбудить. Так что это хороший пример живой блокировки в системе.
Это происходит из-за того, что флаг является полем экземпляра, поэтому не используется совместно потоками. Таким образом, у каждого потока есть собственная копия флага. Если вы пометите его как статическую переменную, то оба потока будут использовать это значение, поэтому проблема будет решена. Вот как должно выглядеть объявление флага.
static boolean flag = false;
Как это решает проблему? Ну, рассмотрим ту же последовательность событий. И теперь Thread1 устанавливает значение флага в true, прежде чем он вызовет уведомление для объекта блокировки. Thread0 уже находится в состоянии ожидания. Расписание отключает Thread1 от ЦП и дает шанс Thread0. Он начинает работать, и, поскольку флаг = true, он входит в цикл while, устанавливает флаг в значение false и вызывает ожидание объекта блокировки. Затем Thread0 переходит в состояние ожидания, а планировщик дает шанс Thread1. Thread1 возобновляет свое выполнение и флаг = false, поэтому он выходит из цикла while, печатая увеличенное значение и уведомляя ожидающий поток. Так что живого замка сейчас нет.
Однако я не вижу смысла использовать как synchronized, так и неблокирующие переменные Atomic. Вы НЕ должны использовать их оба вместе. Более лучшая, производительная реализация приведена ниже.
public class Sequence extends Thread {
private static final Object lock = new Object();
private static int integer = 0;
static boolean flag = false;
@Override
public void run() {
while (true) {
synchronized (lock) {
while (flag) {
flag = false;
try {
System.out.println(Thread.currentThread().getName() + " waiting");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " " + ++integer);
flag = true;
System.out.println(Thread.currentThread().getName() + " notifying");
lock.notify();
}
}
}
public static void main(String[] args) {
Sequence sequence1=new Sequence();
Sequence sequence2=new Sequence();
sequence1.start();
sequence2.start();
}
}
В коде, даже если целое число является атомарным и используется совместно между потоками, сам флаг не является таковым.
class Sequence extends Thread{
private AtomicInteger integer; //shared
boolean flag=false; //local
public Sequence(AtomicInteger integer) {
this.integer=integer;
}
Это приводит к тому, что изменение в одном потоке не отражается в другом.
Предложенное решение:
Вы также можете решить, используя Atomic для флага и обмена, например:
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class StackOverflow {
public static void main(String[] args) {
AtomicInteger integer=new AtomicInteger(0);
AtomicBoolean flag=new AtomicBoolean(true);
Sequence sequence1=new Sequence(integer, flag);
Sequence sequence2=new Sequence(integer, flag);
sequence1.start();
sequence2.start();
}
}
И последовательность:
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
class Sequence extends Thread{
private final AtomicInteger integer;
private AtomicBoolean flag;
public Sequence(AtomicInteger integer, AtomicBoolean flag) {
this.integer=integer;
this.flag=flag;
}
@Override
public void run() {
while(true) {
synchronized (integer) {
while (flag.get()) {
flag.set(false);
try {
System.out.println(Thread.currentThread().getName()+" waiting");
integer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" "+integer.incrementAndGet());
flag.set(true);
System.out.println(Thread.currentThread().getName()+" notifying");
integer.notify();
}
}
}
}
Это часть вывода:
Thread-1 8566
Thread-1 notifying
Thread-1 waiting
Thread-0 8567
Thread-0 notifying
Thread-0 waiting
Thread-1 8568
Thread-1 notifying
Thread-1 waiting
Thread-0 8569
Thread-0 notifying
Thread-0 waiting
На самом деле, у меня только что возникла проблема, да, мы можем сделать флаг таким же доступным, как и вы, между обоими потоками. Но это не проблема. У меня была плохая структура блока while. блок while(true) должен быть внутри блока synchronized и заменить while(flag) на if и все готово. Это был серьезный промах. Тем не менее, код будет работать и с вашим решением, спасибо :)
Вы должны объяснить, как это решает проблему. Но я не вижу здесь такого объяснения.
Какой смысл использовать два разных механизма синхронизации? Почему нельзя использовать статическую переменную?
Но все же я не вижу разумного ответа на свой первый вопрос: «Какой смысл использовать два разных механизма синхронизации?» Внутри синхронизированного блока гарантируется атомарность. Так какой смысл использовать там еще одну переменную Atomic? Какова цель использования этого?
Ваша переменная flag не разделяется между потоками, но логика вокруг этого флага в любом случае странная. Обратите внимание, что вам не нужно использовать AtomicInteger, когда вы используете synchronized.
При правильном использовании synchronized обычной переменной int уже достаточно для реализации всей логики:
public class MultiPrintSequence {
public static void main(String[] args) {
final Sequence sequence = new Sequence();
new Thread(sequence).start();
new Thread(sequence).start();
}
}
class Sequence implements Runnable {
private final Object lock = new Object();
private int sharedNumber;
@Override
public void run() {
synchronized(lock) {
for(;;) {
int myNum = ++sharedNumber;
lock.notify();
System.out.println(Thread.currentThread()+": "+myNum);
while(sharedNumber == myNum) try {
lock.wait();
} catch (InterruptedException ex) {
throw new AssertionError(ex);
}
}
}
}
}
Конечно, создание нескольких потоков для выполнения последовательной операции противоречит фактической цели параллельного программирования.
Общее изменяемое состояние, вот в чем проблема. У вас есть здравый смысл, чтобы правильно разделить целое число, но не для флага.