Многопоточность Java ожидает и уведомляет в одном блоке

Я работаю над небольшой проблемой, когда мне нужно печатать числа последовательно с двумя потоками попеременно. Например, поток 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**

Общее изменяемое состояние, вот в чем проблема. У вас есть здравый смысл, чтобы правильно разделить целое число, но не для флага.

bowmore 24.05.2019 14:00

На самом деле, у меня только что возникла проблема: да, мы можем сделать флаг доступным для обоих потоков. Но это не проблема. У меня была плохая структура блока while. блок while(true) должен быть внутри блока synchronized и заменить while(flag) на if и все готово. Это был серьезный промах. Тем не менее, код будет работать и с вашим решением, спасибо :)

Mayank Vaid 24.05.2019 14:37

ИМО, вам нужно больше думать о том, что означает flag. Оба ваших потока выполняют один и тот же код, и оба они получают один и тот же аргумент. Они идентичны. Но они должны быть разными. Один из них должен печатать только нечетные числа, а другой должен печатать только четные числа. Вам нужен способ сообщить потоку: «Ваша задача — печатать только четные числа» (или «нечетные»); и вам нужен способ, чтобы каждый поток ждал, пока не придет время печатать четное/нечетное число.

Solomon Slow 24.05.2019 14:47

P.S. В качестве дополнительного балла узнайте, почему эта проблема почти полностью противоположна той, для решения которой предназначены потоки. (Подсказка: все, что связано с потоками, связано со словом «одновременно».)

Solomon Slow 24.05.2019 14:51
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
3
4
223
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Рассмотрим эту неудачную последовательность событий. 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 и все готово. Это был серьезный промах. Тем не менее, код будет работать и с вашим решением, спасибо :)

Mayank Vaid 24.05.2019 14:37

Вы должны объяснить, как это решает проблему. Но я не вижу здесь такого объяснения.

Ravindra Ranwala 24.05.2019 15:42

Какой смысл использовать два разных механизма синхронизации? Почему нельзя использовать статическую переменную?

Ravindra Ranwala 24.05.2019 16:01

Но все же я не вижу разумного ответа на свой первый вопрос: «Какой смысл использовать два разных механизма синхронизации?» Внутри синхронизированного блока гарантируется атомарность. Так какой смысл использовать там еще одну переменную Atomic? Какова цель использования этого?

Ravindra Ranwala 24.05.2019 16:36

Ваша переменная 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);
                }
            }
        }
    }
}

Конечно, создание нескольких потоков для выполнения последовательной операции противоречит фактической цели параллельного программирования.

Другие вопросы по теме